diff --git a/api.planx.uk/lib/hasura/metadata/index.ts b/api.planx.uk/lib/hasura/metadata/index.ts index a14f4469da..c0d0594a28 100644 --- a/api.planx.uk/lib/hasura/metadata/index.ts +++ b/api.planx.uk/lib/hasura/metadata/index.ts @@ -1,3 +1,4 @@ +import type { SendIntegration } from "@opensystemslab/planx-core/types"; import type { AxiosResponse } from "axios"; import axios, { isAxiosError } from "axios"; @@ -10,13 +11,9 @@ interface ScheduledEvent { args: ScheduledEventArgs; } -export interface CombinedResponse { - bops?: ScheduledEventResponse; - uniform?: ScheduledEventResponse; - idox?: ScheduledEventResponse; - email?: ScheduledEventResponse; - s3?: ScheduledEventResponse; -} +export type CombinedResponse = Partial< + Record +>; interface ScheduledEventArgs { headers: Record[]; diff --git a/api.planx.uk/modules/pay/service/inviteToPay/createPaymentSendEvents.ts b/api.planx.uk/modules/pay/service/inviteToPay/createPaymentSendEvents.ts index bdaa1c3cbf..092ea8854e 100644 --- a/api.planx.uk/modules/pay/service/inviteToPay/createPaymentSendEvents.ts +++ b/api.planx.uk/modules/pay/service/inviteToPay/createPaymentSendEvents.ts @@ -1,4 +1,4 @@ -import type { Team } from "@opensystemslab/planx-core/types"; +import type { SendIntegration, Team } from "@opensystemslab/planx-core/types"; import { ComponentType } from "@opensystemslab/planx-core/types"; import type { NextFunction, Request, Response } from "express"; import { gql } from "graphql-request"; @@ -8,12 +8,6 @@ import { $api, $public } from "../../../../client/index.js"; import { getMostRecentPublishedFlow } from "../../../../helpers.js"; import type { Flow, Node } from "../../../../types.js"; -enum Destination { - BOPS = "bops", - Uniform = "uniform", - Email = "email", -} - // Create "One-off Scheduled Events" in Hasura when a payment request is paid const createPaymentSendEvents = async ( req: Request, @@ -52,32 +46,32 @@ const createPaymentSendEvents = async ( const sendNode: [string, Node] | undefined = Object.entries( publishedFlowData, ).find(([_nodeId, nodeData]) => nodeData.type === ComponentType.Send); - const destinations: Destination[] = sendNode?.[1]?.data?.destinations; + const destinations: SendIntegration[] = sendNode?.[1]?.data?.destinations; let teamSlug = await getTeamSlugByFlowId(session.flow.id); const eventPayload = { sessionId: payload.sessionId }; - if (destinations.includes(Destination.BOPS)) { + if (destinations.includes("bops")) { const bopsEvent = await createScheduledEvent({ webhook: `{{HASURA_PLANX_API_URL}}/bops/${teamSlug}`, schedule_at: now, payload: eventPayload, comment: `bops_submission_${payload.sessionId}`, }); - combinedResponse[Destination.BOPS] = bopsEvent; + combinedResponse["bops"] = bopsEvent; } - if (destinations.includes(Destination.Email)) { + if (destinations.includes("email")) { const emailSubmissionEvent = await createScheduledEvent({ webhook: `{{HASURA_PLANX_API_URL}}/email-submission/${teamSlug}`, schedule_at: now, payload: eventPayload, comment: `email_submission_${payload.sessionId}`, }); - combinedResponse[Destination.Email] = emailSubmissionEvent; + combinedResponse["email"] = emailSubmissionEvent; } - if (destinations.includes(Destination.Uniform)) { + if (destinations.includes("uniform")) { // Bucks has 3 instances of Uniform for 4 legacy councils, set teamSlug to pre-merger council name if (teamSlug === "buckinghamshire") { const localAuthorities: string[] = session.data?.passport?.data?.[ @@ -101,7 +95,7 @@ const createPaymentSendEvents = async ( payload: eventPayload, comment: `uniform_submission_${payload.sessionId}`, }); - combinedResponse[Destination.Uniform] = uniformEvent; + combinedResponse["uniform"] = uniformEvent; } return res.json(combinedResponse); diff --git a/api.planx.uk/modules/send/createSendEvents/types.ts b/api.planx.uk/modules/send/createSendEvents/types.ts index 684b819566..3d71833480 100644 --- a/api.planx.uk/modules/send/createSendEvents/types.ts +++ b/api.planx.uk/modules/send/createSendEvents/types.ts @@ -1,6 +1,10 @@ import { z } from "zod"; import type { CombinedResponse } from "../../../lib/hasura/metadata/index.js"; import type { ValidatedRequestHandler } from "../../../shared/middleware/validate.js"; +import { + SEND_INTEGRATIONS, + type SendIntegration, +} from "@opensystemslab/planx-core/types"; const eventSchema = z.object({ localAuthority: z.string(), @@ -9,14 +13,19 @@ const eventSchema = z.object({ }), }); +/** Iterate over all possible SendIntegrations to generate the body for this endpoint */ +const bodySchema = z.object( + SEND_INTEGRATIONS.reduce( + (acc, integration) => { + acc[integration] = eventSchema.optional(); + return acc; + }, + {} as Record>, + ), +); + export const combinedEventsPayloadSchema = z.object({ - body: z.object({ - email: eventSchema.optional(), - bops: eventSchema.optional(), - uniform: eventSchema.optional(), - s3: eventSchema.optional(), - idox: eventSchema.optional(), - }), + body: bodySchema, params: z.object({ sessionId: z.string().uuid(), }), diff --git a/api.planx.uk/package.json b/api.planx.uk/package.json index 6e35d6fa51..53e4179030 100644 --- a/api.planx.uk/package.json +++ b/api.planx.uk/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@airbrake/node": "^2.1.8", - "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#4cc216f", + "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#4916ba8", "@types/isomorphic-fetch": "^0.0.36", "adm-zip": "^0.5.10", "aws-sdk": "^2.1467.0", diff --git a/api.planx.uk/pnpm-lock.yaml b/api.planx.uk/pnpm-lock.yaml index cdf19fd18c..9c22b9706a 100644 --- a/api.planx.uk/pnpm-lock.yaml +++ b/api.planx.uk/pnpm-lock.yaml @@ -14,8 +14,8 @@ dependencies: specifier: ^2.1.8 version: 2.1.8 '@opensystemslab/planx-core': - specifier: git+https://github.com/theopensystemslab/planx-core#4cc216f - version: github.com/theopensystemslab/planx-core/4cc216f + specifier: git+https://github.com/theopensystemslab/planx-core#4916ba8 + version: github.com/theopensystemslab/planx-core/4916ba8 '@types/isomorphic-fetch': specifier: ^0.0.36 version: 0.0.36 @@ -6265,8 +6265,8 @@ packages: resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} dev: false - github.com/theopensystemslab/planx-core/4cc216f: - resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/4cc216f} + github.com/theopensystemslab/planx-core/4916ba8: + resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/4916ba8} name: '@opensystemslab/planx-core' version: 1.0.0 prepare: true diff --git a/e2e/tests/api-driven/package.json b/e2e/tests/api-driven/package.json index 8206c0d534..4d6ccc9b59 100644 --- a/e2e/tests/api-driven/package.json +++ b/e2e/tests/api-driven/package.json @@ -7,7 +7,7 @@ "packageManager": "pnpm@8.6.6", "dependencies": { "@cucumber/cucumber": "^9.3.0", - "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#4cc216f", + "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#4916ba8", "axios": "^1.7.4", "dotenv": "^16.3.1", "dotenv-expand": "^10.0.0", diff --git a/e2e/tests/api-driven/pnpm-lock.yaml b/e2e/tests/api-driven/pnpm-lock.yaml index 24e3695677..e9b1fac8e5 100644 --- a/e2e/tests/api-driven/pnpm-lock.yaml +++ b/e2e/tests/api-driven/pnpm-lock.yaml @@ -9,8 +9,8 @@ dependencies: specifier: ^9.3.0 version: 9.3.0 '@opensystemslab/planx-core': - specifier: git+https://github.com/theopensystemslab/planx-core#4cc216f - version: github.com/theopensystemslab/planx-core/4cc216f + specifier: git+https://github.com/theopensystemslab/planx-core#4916ba8 + version: github.com/theopensystemslab/planx-core/4916ba8 axios: specifier: ^1.7.4 version: 1.7.4 @@ -2956,8 +2956,8 @@ packages: resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} dev: false - github.com/theopensystemslab/planx-core/4cc216f: - resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/4cc216f} + github.com/theopensystemslab/planx-core/4916ba8: + resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/4916ba8} name: '@opensystemslab/planx-core' version: 1.0.0 prepare: true diff --git a/e2e/tests/ui-driven/package.json b/e2e/tests/ui-driven/package.json index a59ab00436..d0c69e9bad 100644 --- a/e2e/tests/ui-driven/package.json +++ b/e2e/tests/ui-driven/package.json @@ -8,7 +8,7 @@ "postinstall": "./install-dependencies.sh" }, "dependencies": { - "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#4cc216f", + "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#4916ba8", "axios": "^1.7.4", "dotenv": "^16.3.1", "eslint": "^8.56.0", diff --git a/e2e/tests/ui-driven/pnpm-lock.yaml b/e2e/tests/ui-driven/pnpm-lock.yaml index f7f4cd6bff..8d39192f95 100644 --- a/e2e/tests/ui-driven/pnpm-lock.yaml +++ b/e2e/tests/ui-driven/pnpm-lock.yaml @@ -6,8 +6,8 @@ settings: dependencies: '@opensystemslab/planx-core': - specifier: git+https://github.com/theopensystemslab/planx-core#4cc216f - version: github.com/theopensystemslab/planx-core/4cc216f + specifier: git+https://github.com/theopensystemslab/planx-core#4916ba8 + version: github.com/theopensystemslab/planx-core/4916ba8 axios: specifier: ^1.7.4 version: 1.7.4 @@ -2695,8 +2695,8 @@ packages: resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} dev: false - github.com/theopensystemslab/planx-core/4cc216f: - resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/4cc216f} + github.com/theopensystemslab/planx-core/4916ba8: + resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/4916ba8} name: '@opensystemslab/planx-core' version: 1.0.0 prepare: true diff --git a/editor.planx.uk/package.json b/editor.planx.uk/package.json index f938daf5c0..2e4d5f60ac 100644 --- a/editor.planx.uk/package.json +++ b/editor.planx.uk/package.json @@ -15,7 +15,7 @@ "@mui/material": "^5.15.10", "@mui/utils": "^5.15.11", "@opensystemslab/map": "1.0.0-alpha.3", - "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#4cc216f", + "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#4916ba8", "@tiptap/core": "^2.4.0", "@tiptap/extension-bold": "^2.0.3", "@tiptap/extension-bubble-menu": "^2.1.13", diff --git a/editor.planx.uk/pnpm-lock.yaml b/editor.planx.uk/pnpm-lock.yaml index aab00a8439..b532db8301 100644 --- a/editor.planx.uk/pnpm-lock.yaml +++ b/editor.planx.uk/pnpm-lock.yaml @@ -47,8 +47,8 @@ dependencies: specifier: 1.0.0-alpha.3 version: 1.0.0-alpha.3 '@opensystemslab/planx-core': - specifier: git+https://github.com/theopensystemslab/planx-core#4cc216f - version: github.com/theopensystemslab/planx-core/4cc216f(@types/react@18.2.45) + specifier: git+https://github.com/theopensystemslab/planx-core#4916ba8 + version: github.com/theopensystemslab/planx-core/4916ba8(@types/react@18.2.45) '@tiptap/core': specifier: ^2.4.0 version: 2.4.0(@tiptap/pm@2.0.3) @@ -15359,9 +15359,9 @@ packages: use-sync-external-store: 1.2.0(react@18.2.0) dev: false - github.com/theopensystemslab/planx-core/4cc216f(@types/react@18.2.45): - resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/4cc216f} - id: github.com/theopensystemslab/planx-core/4cc216f + github.com/theopensystemslab/planx-core/4916ba8(@types/react@18.2.45): + resolution: {tarball: https://codeload.github.com/theopensystemslab/planx-core/tar.gz/4916ba8} + id: github.com/theopensystemslab/planx-core/4916ba8 name: '@opensystemslab/planx-core' version: 1.0.0 prepare: true diff --git a/editor.planx.uk/src/@planx/components/Send/Editor.tsx b/editor.planx.uk/src/@planx/components/Send/Editor.tsx index 964e434c86..0747a656e3 100644 --- a/editor.planx.uk/src/@planx/components/Send/Editor.tsx +++ b/editor.planx.uk/src/@planx/components/Send/Editor.tsx @@ -2,7 +2,7 @@ import Warning from "@mui/icons-material/Warning"; import Box from "@mui/material/Box"; import Grid from "@mui/material/Grid"; import Typography from "@mui/material/Typography"; -import { ComponentType as TYPES } from "@opensystemslab/planx-core/types"; +import { ComponentType as TYPES, SendIntegration } from "@opensystemslab/planx-core/types"; import { getIn, useFormik } from "formik"; import React from "react"; import ModalSection from "ui/editor/ModalSection"; @@ -14,7 +14,7 @@ import InputRow from "ui/shared/InputRow"; import { array, object } from "yup"; import { EditorProps, ICONS } from "../ui"; -import { Destination, Send } from "./model"; +import { Send } from "./model"; import { parseContent } from "./model"; export type Props = EditorProps; @@ -35,40 +35,40 @@ const SendComponent: React.FC = (props) => { .test({ name: "atLeastOneChecked", message: "Select at least one destination", - test: (destinations?: Array) => { + test: (destinations?: Array) => { return Boolean(destinations && destinations.length > 0); }, }), }), }); - const options: { value: Destination; label: string }[] = [ + const options: { value: SendIntegration; label: string }[] = [ { - value: Destination.BOPS, + value: "bops", label: "BOPS", }, { - value: Destination.Uniform, + value: "uniform", label: "Uniform", }, { - value: Destination.Idox, + value: "idox", label: "Idox Nexus (TESTING ONLY)", }, { - value: Destination.Email, + value: "email", label: "Email to planning office", }, { - value: Destination.S3, + value: "s3", label: "Upload to AWS S3 bucket", }, ]; const changeCheckbox = - (value: Destination) => + (value: SendIntegration) => (_checked: React.MouseEvent | undefined) => { - let newCheckedValues: Destination[]; + let newCheckedValues: SendIntegration[]; if (formik.values.destinations.includes(value)) { newCheckedValues = formik.values.destinations.filter( @@ -90,14 +90,14 @@ const SendComponent: React.FC = (props) => { // Don't actually restrict selection because flowSlug matching is imperfect for some valid test cases const teamSlug = window.location.pathname?.split("/")?.[1]; const flowSlug = window.location.pathname?.split("/")?.[2]; - if (value === Destination.BOPS && newCheckedValues.includes(value)) { + if (value === "bops" && newCheckedValues.includes(value)) { alert( "BOPS only accepts Lawful Development Certificate, Prior Approval, and Planning Permission submissions. Please do not select if you're building another type of submission service!", ); } if ( - value === Destination.Uniform && + value === "uniform" && newCheckedValues.includes(value) && flowSlug !== "apply-for-a-lawful-development-certificate" && !["buckinghamshire", "lambeth", "southwark"].includes(teamSlug) @@ -108,7 +108,7 @@ const SendComponent: React.FC = (props) => { } if ( - value === Destination.S3 && + value === "s3" && newCheckedValues.includes(value) && !["barnet", "lambeth"].includes(teamSlug) ) { diff --git a/editor.planx.uk/src/@planx/components/Send/Public.test.tsx b/editor.planx.uk/src/@planx/components/Send/Public.test.tsx index 79e7294de7..00ba3d8ac1 100644 --- a/editor.planx.uk/src/@planx/components/Send/Public.test.tsx +++ b/editor.planx.uk/src/@planx/components/Send/Public.test.tsx @@ -1,36 +1,228 @@ +import { SendIntegration } from "@opensystemslab/planx-core/types"; +import { waitFor } from "@testing-library/react"; import axios from "axios"; +import { FullStore, useStore } from "pages/FlowEditor/lib/store"; import React from "react"; +import { act } from "react-dom/test-utils"; import { setup } from "testUtils"; import { vi } from "vitest"; import { axe } from "vitest-axe"; import hasuraEventsResponseMock from "./mocks/hasuraEventsResponseMock"; -import { Destination } from "./model"; +import { flow } from "./mocks/simpleFlow"; import SendComponent from "./Public"; +const { getState, setState } = useStore; + +let initialState: FullStore; + +/** + * Adds a small tick to allow MUI to render (e.g. card transitions) + */ +const tick = () => act(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); +}); + vi.mock("axios"); const mockAxios = vi.mocked(axios, true); -mockAxios.post.mockResolvedValue(async (url: string) => { - return { - value: url.startsWith( - `${import.meta.env.VITE_APP_API_URL}/create-send-events/`, - ) - ? hasuraEventsResponseMock - : null, - }; +mockAxios.post.mockResolvedValue({ + data: hasuraEventsResponseMock, + status: 200, + statusText: "OK", }); -it.todo("renders correctly"); +const originalLocation = window.location.pathname; + +beforeAll(() => (initialState = getState())); -it.todo("sets :localAuthority API param correctly based on team or passport"); +beforeEach(() => (act(() => setState({ teamSlug: "testTeam" })))); + +afterEach(() => { + vi.clearAllMocks(); + window.history.pushState({}, "", originalLocation); + act(() => setState(initialState)) +}); + +it("displays a warning at /draft URLs", () => { + window.history.pushState({}, "", "/draft"); + const { getByText } = setup( + , + ); + + expect(getByText(/You can only test submissions on/)).toBeVisible(); +}); + +it("displays a warning at /preview URLs", () => { + window.history.pushState({}, "", "/preview"); + const { getByText } = setup( + , + ); + + expect(getByText(/You can only test submissions on/)).toBeVisible(); +}); + +it("displays loading messages to the user", async () => { + let resolvePromise: (value: any) => void; + const promise = new Promise((resolve) => { + resolvePromise = resolve; + }); + + mockAxios.post.mockImplementationOnce(() => promise); + + const { getByText } = setup( + , + ); + + await tick(); + + // Initial loading state + expect(getByText(/Submitting your application.../)).toBeVisible(); + + // Trigger mock API response + resolvePromise!({ + data: hasuraEventsResponseMock, + status: 200, + statusText: "OK", + }); + + expect(mockAxios.post).toHaveBeenCalledTimes(1); + + await tick(); + + // Final submission state + expect(getByText(/Finalising your submission.../)).toBeVisible(); +}); + +it("calls the /create-send-event endpoint", async () => { + setup( + , + ); + + await waitFor(() => expect(mockAxios.post).toHaveBeenCalledTimes(1)); + + expect(mockAxios.post).toHaveBeenCalledTimes(1); +}); + +it("generates a valid payload for the API", async () => { + const destinations: SendIntegration[] = ["bops", "uniform"]; + + setup( + , + ); + + await waitFor(() => expect(mockAxios.post).toHaveBeenCalledTimes(1)); + + const apiPayload = mockAxios.post.mock.calls[0][1]; + + destinations.forEach(destination => { + expect(apiPayload).toHaveProperty(destination); + expect((apiPayload as Record)[destination]).toHaveProperty("localAuthority", "testTeam"); + }); +}); + +describe("Uniform overrides for Buckinghamshire", () => { + it("converts property.localAuthorityDistrict to the correct format", async () => { + act(() => setState({ + teamSlug: "buckinghamshire", + flow, + breadcrumbs: { + findProperty: { + data: { + "property.localAuthorityDistrict": ["Buckinghamshire", "Historic district name"] + } + } + } + })); + + setup( + , + ); + + await waitFor(() => expect(mockAxios.post).toHaveBeenCalledTimes(1)); + + const apiPayload = mockAxios.post.mock.calls[0][1] as any; + + // BOPS event not modified + expect(apiPayload?.bops?.localAuthority).toEqual("buckinghamshire"); + + // Uniform event has read property.localAuthorityDistrict from the passport + expect(apiPayload?.uniform?.localAuthority).toEqual("historic-district-name"); + }); + + it("maps requests for South Bucks to Chiltern", async () => { + act(() => setState({ + teamSlug: "buckinghamshire", + flow, + breadcrumbs: { + findProperty: { + data: { + "property.localAuthorityDistrict": ["South Bucks"] + } + } + } + })); + + setup( + , + ); + + await waitFor(() => expect(mockAxios.post).toHaveBeenCalledTimes(1)); + + const apiPayload = mockAxios.post.mock.calls[0][1] as any; + + expect(apiPayload?.uniform?.localAuthority).toEqual("chiltern"); + }); +}) + +it("generates a valid breadcrumb", async () => { + const handleSubmit = vi.fn(); + + setup( + , + ); + + await waitFor(() => expect(mockAxios.post).toHaveBeenCalledTimes(1)); + expect(handleSubmit).toHaveBeenCalledTimes(1); + + const breadcrumb = handleSubmit.mock.calls[0][0]; + + expect(breadcrumb.data).toEqual(expect.objectContaining({ + bopsSendEventId: hasuraEventsResponseMock.bops.event_id, + uniformSendEventId: hasuraEventsResponseMock.uniform.event_id, + })); +}); -// TODO: Turn this test back on when Uniform payload generation is moved to API it("should not have any accessibility violations", async () => { const { container } = setup( , ); const results = await axe(container); diff --git a/editor.planx.uk/src/@planx/components/Send/Public.tsx b/editor.planx.uk/src/@planx/components/Send/Public.tsx index 854d33de26..3e1ceedc93 100644 --- a/editor.planx.uk/src/@planx/components/Send/Public.tsx +++ b/editor.planx.uk/src/@planx/components/Send/Public.tsx @@ -1,22 +1,28 @@ import ErrorOutline from "@mui/icons-material/ErrorOutline"; import Typography from "@mui/material/Typography"; -import axios from "axios"; +import { SendIntegration } from "@opensystemslab/planx-core/types"; +import axios, { AxiosResponse } from "axios"; import DelayedLoadingIndicator from "components/DelayedLoadingIndicator"; import { useStore } from "pages/FlowEditor/lib/store"; import React, { useEffect } from "react"; import { useAsync } from "react-use"; +import { AsyncState } from "react-use/lib/useAsyncFn"; import Card from "../shared/Preview/Card"; import { WarningContainer } from "../shared/Preview/WarningContainer"; -import { makeData } from "../shared/utils"; import { PublicProps } from "../ui"; import { DEFAULT_DESTINATION, - Destination, getCombinedEventsPayload, Send, } from "./model"; +/** Response returned by /create-send-events endpoint */ +type SendResponse = Record; + +/** State generated by useAsync to hold SendResponse */ +type SendRequestState = AsyncState> + export type Props = PublicProps; const SendComponent: React.FC = ({ @@ -61,10 +67,9 @@ const CreateSendEvents: React.FC = ({ ]); // Send makes a single request to create scheduled events in Hasura, then those events make the actual submission requests with retries etc - const url = `${ - import.meta.env.VITE_APP_API_URL - }/create-send-events/${sessionId}`; - const request: any = useAsync(async () => { + const url = `${import.meta.env.VITE_APP_API_URL + }/create-send-events/${sessionId}`; + const { loading, error, value }: SendRequestState = useAsync(async () => { const combinedEventsPayload = getCombinedEventsPayload({ destinations, teamSlug, @@ -76,75 +81,36 @@ const CreateSendEvents: React.FC = ({ }); useEffect(() => { - const isReady = !request.loading && !request.error && request.value; - - if ( - destinations.includes(Destination.BOPS) && - isReady && - props.handleSubmit - ) { - props.handleSubmit( - makeData(props, request.value.bops?.event_id, "bopsSendEventId"), - ); - } - - if ( - destinations.includes(Destination.Uniform) && - isReady && - props.handleSubmit - ) { - props.handleSubmit( - makeData(props, request.value.uniform?.event_id, "uniformSendEventId"), - ); - } - - if ( - destinations.includes(Destination.Idox) && - isReady && - props.handleSubmit - ) { - props.handleSubmit( - makeData(props, request.value.idox?.event_id, "idoxSendEventId"), - ); - } - - if ( - destinations.includes(Destination.Email) && - isReady && - props.handleSubmit - ) { - props.handleSubmit( - makeData(props, request.value.email?.event_id, "emailSendEventId"), - ); - } - - if ( - destinations.includes(Destination.S3) && - isReady && - props.handleSubmit - ) { - props.handleSubmit( - makeData(props, request.value.s3?.event_id, "s3SendEventId"), - ); - } - }, [request.loading, request.error, request.value, destinations, props]); - - if (request.loading) { - return ( - - - + const isReady = !loading && !error && value; + if (!isReady) return; + + // Construct breadcrumb containing IDs of each send event generated + const data = Object.fromEntries( + destinations.map(destination => [ + `${destination}SendEventId`, + value.data[destination]?.event_id + ]) ); - } else if (request.error) { - // Throw errors so that they're caught by our error boundaries and Airbrake - throw request.error; - } else { + + props.handleSubmit && props?.handleSubmit({ data }); + }, [loading, error, value, destinations, props]); + + // Throw errors so that they're caught by our error boundaries and Airbrake + if (error) throw error; + + if (loading) { return ( - + ); - } + }; + + return ( + + + + ); }; export default SendComponent; diff --git a/editor.planx.uk/src/@planx/components/Send/mocks/hasuraEventsResponseMock.ts b/editor.planx.uk/src/@planx/components/Send/mocks/hasuraEventsResponseMock.ts index 78cf5fa60a..ea2593e4f7 100644 --- a/editor.planx.uk/src/@planx/components/Send/mocks/hasuraEventsResponseMock.ts +++ b/editor.planx.uk/src/@planx/components/Send/mocks/hasuraEventsResponseMock.ts @@ -1,8 +1,8 @@ export default { bops: { - event_id: 1, + event_id: "1", }, uniform: { - event_id: 2, + event_id: "2", }, }; diff --git a/editor.planx.uk/src/@planx/components/Send/mocks/simpleFlow.ts b/editor.planx.uk/src/@planx/components/Send/mocks/simpleFlow.ts new file mode 100644 index 0000000000..2199b4b9db --- /dev/null +++ b/editor.planx.uk/src/@planx/components/Send/mocks/simpleFlow.ts @@ -0,0 +1,27 @@ +import { Graph } from "@planx/graph"; + +export const flow: Graph = { + _root: { + edges: ["findProperty", "send"], + }, + send: { + data: { + tags: [], + title: "Send", + destinations: ["bops", "uniform"], + }, + type: 650, + }, + findProperty: { + data: { + title: "Find the property", + newAddressTitle: + "Click or tap at where the property is on the map and name it below", + allowNewAddresses: false, + newAddressDescription: + "You will need to select a location and provide a name to continue", + newAddressDescriptionLabel: "Name the site", + }, + type: 9, + }, +}; \ No newline at end of file diff --git a/editor.planx.uk/src/@planx/components/Send/model.ts b/editor.planx.uk/src/@planx/components/Send/model.ts index 31929e0808..8a85dcfe0a 100644 --- a/editor.planx.uk/src/@planx/components/Send/model.ts +++ b/editor.planx.uk/src/@planx/components/Send/model.ts @@ -1,13 +1,9 @@ +import { SendIntegration } from "@opensystemslab/planx-core/types"; + import type { Store } from "../../../pages/FlowEditor/lib/store"; import { BaseNodeData, parseBaseNodeData } from "../shared"; -export enum Destination { - BOPS = "bops", - Uniform = "uniform", - Idox = "idox", - Email = "email", - S3 = "s3", -} +type CombinedEventsPayload = Partial>; interface EventPayload { localAuthority: string; @@ -18,11 +14,11 @@ interface EventPayload { export interface Send extends BaseNodeData { title: string; - destinations: Destination[]; + destinations: SendIntegration[]; } export const DEFAULT_TITLE = "Send"; -export const DEFAULT_DESTINATION = Destination.Email; +export const DEFAULT_DESTINATION = "email"; export const parseContent = (data: Record | undefined): Send => ({ ...parseBaseNodeData(data), @@ -30,69 +26,52 @@ export const parseContent = (data: Record | undefined): Send => ({ destinations: data?.destinations || [DEFAULT_DESTINATION], }); +const isSendingToUniform = ( + payload: CombinedEventsPayload +): payload is CombinedEventsPayload & { uniform: EventPayload } => + "uniform" in payload; + export function getCombinedEventsPayload({ destinations, teamSlug, passport, sessionId, }: { - destinations: Destination[]; + destinations: SendIntegration[]; teamSlug: string; passport: Store.Passport; sessionId: string; }) { - const combinedEventsPayload: Record = {}; + const payload: CombinedEventsPayload = {}; - // Format application user data as required by BOPS - if (destinations.includes(Destination.BOPS)) { - combinedEventsPayload[Destination.BOPS] = { + // Construct payload containing details for each send destination + destinations.forEach((destination) => { + payload[destination] = { localAuthority: teamSlug, body: { sessionId }, }; - } + }); - if (destinations.includes(Destination.Email)) { - combinedEventsPayload[Destination.Email] = { - localAuthority: teamSlug, - body: { sessionId }, - }; - } + // Bucks has 3 instances of Uniform for 4 legacy councils, set teamSlug to pre-merger council name + const isUniformOverrideRequired = + isSendingToUniform(payload) && teamSlug === "buckinghamshire"; - // Format application user data as required by Idox/Uniform - if (destinations.includes(Destination.Uniform)) { + if (isUniformOverrideRequired) { let uniformTeamSlug = teamSlug; - // Bucks has 3 instances of Uniform for 4 legacy councils, set teamSlug to pre-merger council name - if (uniformTeamSlug === "buckinghamshire") { - uniformTeamSlug = passport.data?.["property.localAuthorityDistrict"] - ?.filter((name: string) => name !== "Buckinghamshire")[0] - ?.toLowerCase() - ?.replace(/\W+/g, "-"); - // South Bucks & Chiltern share an Idox connector, route addresses in either to Chiltern - if (uniformTeamSlug === "south-bucks") { - uniformTeamSlug = "chiltern"; - } - } + uniformTeamSlug = passport.data?.["property.localAuthorityDistrict"] + ?.filter((name: string) => name !== "Buckinghamshire")[0] + ?.toLowerCase() + ?.replace(/\W+/g, "-"); - combinedEventsPayload[Destination.Uniform] = { - localAuthority: uniformTeamSlug, - body: { sessionId }, - }; - } - - if (destinations.includes(Destination.Idox)) { - combinedEventsPayload[Destination.Idox] = { - localAuthority: teamSlug, - body: { sessionId }, - }; - } + // South Bucks & Chiltern share an Idox connector, route addresses in either to Chiltern + if (uniformTeamSlug === "south-bucks") { + uniformTeamSlug = "chiltern"; + } - if (destinations.includes(Destination.S3)) { - combinedEventsPayload[Destination.S3] = { - localAuthority: teamSlug, - body: { sessionId }, - }; + // Apply override + payload["uniform"].localAuthority = uniformTeamSlug; } - return combinedEventsPayload; + return payload; }