From 59c9f5057336b147062cda618c4324ae1c0016ad Mon Sep 17 00:00:00 2001 From: Jessica McInchak Date: Fri, 17 May 2024 12:00:11 +0100 Subject: [PATCH] feat: submissions feed reflects events rather than successful audit entries (#3153) Co-authored-by: Ian Jones <51156018+ianjon3s@users.noreply.github.com> --- api.planx.uk/modules/send/email/index.test.ts | 4 +- api.planx.uk/modules/send/email/index.ts | 4 +- api.planx.uk/modules/send/s3/index.ts | 2 +- .../Settings/Submissions/EventsLog.tsx | 185 ++++++++++++++++++ .../Submissions/SubmissionView.stories.tsx | 46 ----- .../Submissions/SubmissionsTable.stories.tsx | 18 -- .../Submissions/SubmissionsTable.test.tsx | 85 -------- .../Settings/Submissions/SubmissionsTable.tsx | 150 -------------- .../Submissions/SubmissionsView.test.tsx | 61 ------ .../Settings/Submissions/SubmissionsView.tsx | 31 --- .../Settings/Submissions/index.stories.tsx | 69 ------- .../Settings/Submissions/index.test.tsx | 85 -------- .../components/Settings/Submissions/index.tsx | 113 ++++++++--- .../components/Settings/Submissions/mocks.ts | 145 -------------- .../Settings/Submissions/submissionData.ts | 93 --------- hasura.planx.uk/metadata/tables.yaml | 27 +++ .../down.sql | 45 +++++ .../up.sql | 65 ++++++ 18 files changed, 414 insertions(+), 814 deletions(-) create mode 100644 editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/EventsLog.tsx delete mode 100644 editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/SubmissionView.stories.tsx delete mode 100644 editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/SubmissionsTable.stories.tsx delete mode 100644 editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/SubmissionsTable.test.tsx delete mode 100644 editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/SubmissionsTable.tsx delete mode 100644 editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/SubmissionsView.test.tsx delete mode 100644 editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/SubmissionsView.tsx delete mode 100644 editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/index.stories.tsx delete mode 100644 editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/index.test.tsx delete mode 100644 editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/mocks.ts delete mode 100644 editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/submissionData.ts create mode 100644 hasura.planx.uk/migrations/1715874150959_update submission services log/down.sql create mode 100644 hasura.planx.uk/migrations/1715874150959_update submission services log/up.sql diff --git a/api.planx.uk/modules/send/email/index.test.ts b/api.planx.uk/modules/send/email/index.test.ts index 4481c258c1..a50aefe3c0 100644 --- a/api.planx.uk/modules/send/email/index.test.ts +++ b/api.planx.uk/modules/send/email/index.test.ts @@ -118,7 +118,9 @@ describe(`sending an application by email to a planning office`, () => { .expect(200) .then((res) => { expect(res.body).toEqual({ - message: 'Successfully sent "Submit" email', + message: `Successfully sent to email`, + inbox: "planners@southwark.gov.uk", + govuk_notify_template: "Submit", }); }); }); diff --git a/api.planx.uk/modules/send/email/index.ts b/api.planx.uk/modules/send/email/index.ts index 93df80543f..3794eee09f 100644 --- a/api.planx.uk/modules/send/email/index.ts +++ b/api.planx.uk/modules/send/email/index.ts @@ -69,7 +69,9 @@ export async function sendToEmail( ); return res.status(200).send({ - message: `Successfully sent "Submit" email`, + message: `Successfully sent to email`, + inbox: sendToEmail, + govuk_notify_template: "Submit", }); } catch (error) { return next({ diff --git a/api.planx.uk/modules/send/s3/index.ts b/api.planx.uk/modules/send/s3/index.ts index 3f074bd5c7..350fc43629 100644 --- a/api.planx.uk/modules/send/s3/index.ts +++ b/api.planx.uk/modules/send/s3/index.ts @@ -36,7 +36,7 @@ export async function sendToS3( if (!powerAutomateWebhookURL || !powerAutomateAPIKey) { return next({ status: 400, - message: `Send to S3 is not enabled for this local authority (${localAuthority})`, + message: `Upload to S3 is not enabled for this local authority (${localAuthority})`, }); } diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/EventsLog.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/EventsLog.tsx new file mode 100644 index 0000000000..4943bf092c --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/EventsLog.tsx @@ -0,0 +1,185 @@ +import KeyboardArrowDown from "@mui/icons-material/KeyboardArrowDown"; +import KeyboardArrowUp from "@mui/icons-material/KeyboardArrowUp"; +import Payment from "@mui/icons-material/Payment"; +import Send from "@mui/icons-material/Send"; +import Box from "@mui/material/Box"; +import Chip from "@mui/material/Chip"; +import Collapse from "@mui/material/Collapse"; +import IconButton from "@mui/material/IconButton"; +import { styled } from "@mui/material/styles"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TableRow from "@mui/material/TableRow"; +import Typography from "@mui/material/Typography"; +import DelayedLoadingIndicator from "components/DelayedLoadingIndicator"; +import ErrorFallback from "components/ErrorFallback"; +import { format } from "date-fns"; +import React, { useState } from "react"; + +import { GetSubmissionsResponse, Submission } from "."; + +const ErrorSummary = styled(Box)(({ theme }) => ({ + marginTop: theme.spacing(1), + padding: theme.spacing(3), + border: `5px solid ${theme.palette.error.main}`, +})); + +const Response = styled(Box)(() => ({ + fontSize: "1em", + margin: 1, + maxWidth: "contentWrap", + overflowWrap: "break-word", + whiteSpace: "pre-wrap", +})); + +// Style the table container like an event feed with bottom-anchored scroll +const Feed = styled(TableContainer)(() => ({ + maxHeight: "60vh", + overflow: "auto", + display: "flex", + flexDirection: "column-reverse", + readingOrder: "flex-visual", +})); + +const EventsLog: React.FC = ({ + submissions, + loading, + error, +}) => { + if (loading) + return ( + + ); + if (error) return ; + if (submissions.length === 0) + return ( + + + {`No payments or submissions found for this service`} + + + {`If you're looking for events before January 1, 2024, please contact a PlanX developer.`} + + + ); + + return ( + + + + *": { borderBottomColor: "black !important" } }}> + + Event + + + Status + + + Date + + + Session ID + + + + + + {submissions.map((submission, i) => ( + + ))} + +
+
+ ); +}; + +const CollapsibleRow: React.FC = (submission) => { + const [open, setOpen] = useState(false); + + return ( + + *": { borderBottom: "unset" } }}> + + + {submission.eventType === "Pay" ? : } + + {submission.eventType} {submission.retry && ` [Retry]`} + + + + + {submission.status === "Success" ? ( + + ) : ( + + )} + + + {format(new Date(submission.createdAt), "dd/MM/yy hh:mm:ss")} + + {submission.sessionId} + + setOpen(!open)} + > + {open ? : } + + + + theme.palette.background.paper }}> + + + `1px solid ${theme.palette.border.light}`, + padding: (theme) => theme.spacing(0, 1.5), + }} + > + + + + + + ); +}; + +const FormattedResponse: React.FC = (submission) => { + if (submission.eventType === "Pay") { + return ( + + {JSON.stringify(submission.response, null, 2)} + + ); + } else { + return ( + + {submission.status === "Success" + ? JSON.stringify(JSON.parse(submission.response?.data?.body), null, 2) + : JSON.stringify( + JSON.parse(submission.response?.data?.message), + null, + 2, + )} + + ); + } +}; + +export default EventsLog; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/SubmissionView.stories.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/SubmissionView.stories.tsx deleted file mode 100644 index 8f48c5cd91..0000000000 --- a/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/SubmissionView.stories.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Meta, StoryObj } from "@storybook/react"; -import React from "react"; - -import { mockApplications } from "./mocks"; -import SubmissionsView from "./SubmissionsView"; - -const meta: Meta = { - title: "Design System/Molecules/SubmissionsView", - component: SubmissionsView, -}; - -type Story = StoryObj; - -export default meta; - -export const DefaultView: Story = { - render: () => ( - - ), -}; - -export const Loading: Story = { - render: () => ( - - ), -}; - -export const ErrorState: Story = { - render: () => ( - - ), -}; - -export const Empty: Story = { - render: () => ( - - ), -}; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/SubmissionsTable.stories.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/SubmissionsTable.stories.tsx deleted file mode 100644 index fff4b13d05..0000000000 --- a/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/SubmissionsTable.stories.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Meta, StoryObj } from "@storybook/react"; -import React from "react"; - -import { mockApplications } from "./mocks"; -import SubmissionsTable from "./SubmissionsTable"; - -const meta: Meta = { - title: "Design System/Molecules/SubmissionsTable", - component: SubmissionsTable, -}; - -type Story = StoryObj; - -export default meta; - -export const Basic: Story = { - render: () => , -}; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/SubmissionsTable.test.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/SubmissionsTable.test.tsx deleted file mode 100644 index 90852b7098..0000000000 --- a/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/SubmissionsTable.test.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import React from "react"; -import { axe, setup } from "testUtils"; - -import { mockApplications } from "./mocks"; -import SubmissionsTable from "./SubmissionsTable"; - -describe("SubmissionsTable renders as expected", () => { - test("renders expected table headers", () => { - const { getByText } = setup( - , - ); - expect(getByText("Session ID")).toBeInTheDocument(); - expect(getByText("Submitted At")).toBeInTheDocument(); - expect(getByText("Payment Requests")).toBeInTheDocument(); - expect(getByText("Payment Status")).toBeInTheDocument(); - expect(getByText("BOPS Applications")).toBeInTheDocument(); - expect(getByText("Uniform Applications")).toBeInTheDocument(); - expect(getByText("Email Applications")).toBeInTheDocument(); - }); - - test("renders the session ids", () => { - const { getByText } = setup( - , - ); - - mockApplications.forEach((app) => { - expect(getByText(app.sessionId)).toBeInTheDocument(); - }); - }); - - test("renders payment request ids when available", () => { - const { getByText } = setup( - , - ); - - expect(getByText(/test-payment-request-2/)).toBeInTheDocument(); - expect(getByText(/test-payment-request-1/)).toBeInTheDocument(); - }); - - test("renders the latest payment status for each session", () => { - const { getAllByText } = setup( - , - ); - - expect(getAllByText(/Status: created/)).toHaveLength(3); - expect(getAllByText(/Status: error/)).toHaveLength(1); - expect(getAllByText(/Status: success/)).toHaveLength(2); - }); - - test("renders bops application ids when available", () => { - const { getByText } = setup( - , - ); - - expect(getByText(/test-bops-1/)).toBeInTheDocument(); - }); - - test("renders uniform application ids when available", () => { - const { getByText } = setup( - , - ); - - expect(getByText(/test-uniform-1/)).toBeInTheDocument(); - expect(getByText(/test-uniform-2/)).toBeInTheDocument(); - }); - - test("renders email application recipients when available", () => { - const { getByText } = setup( - , - ); - - expect(getByText(/test-user-1@opensystemslab.io/)).toBeInTheDocument(); - expect(getByText(/test-user-2@opensystemslab.io/)).toBeInTheDocument(); - expect(getByText(/test-user-3@opensystemslab.io/)).toBeInTheDocument(); - }); - - test("renders with no accessibility violations", async () => { - const { container } = setup( - , - ); - - const results = await axe(container); - expect(results).toHaveNoViolations(); - }); -}); diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/SubmissionsTable.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/SubmissionsTable.tsx deleted file mode 100644 index 21624b2297..0000000000 --- a/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/SubmissionsTable.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import Box from "@mui/material/Box"; -import { styled } from "@mui/material/styles"; -import Table from "@mui/material/Table"; -import TableBody from "@mui/material/TableBody"; -import TableCell from "@mui/material/TableCell"; -import TableContainer from "@mui/material/TableContainer"; -import TableHead from "@mui/material/TableHead"; -import TableRow from "@mui/material/TableRow"; -import React from "react"; -import { FONT_WEIGHT_SEMI_BOLD } from "theme"; - -import { SubmissionData } from "./submissionData"; - -const StyledTableContainer = styled(TableContainer)(({ theme }) => ({ - border: `1px solid ${theme.palette.divider}`, - maxHeight: "70vh", - "&:focus": { - outline: `2px solid ${theme.palette.primary.main}`, - }, -})); - -const StyledTable = styled(Table)(({ theme }) => ({ - borderCollapse: "collapse", - "& th": { - background: theme.palette.text.primary, - color: theme.palette.common.white, - fontWeight: FONT_WEIGHT_SEMI_BOLD, - }, - "& td": { - borderBottomColor: theme.palette.border.main, - borderBottomWidth: "2px", - }, - "& tr:nth-child(odd) td": { - background: theme.palette.background.paper, - }, -})); - -const StyledTableCell = styled(TableCell)(({ theme }) => ({ - border: `1px solid ${theme.palette.divider}`, - lineHeight: "1.35", -})); - -const DividerStyled = styled(Box)({ - paddingBottom: "8px", - marginBottom: "8px", - borderBottom: "1px solid #eee", - "&:last-child": { - border: "none", - paddingBottom: "0", - marginBottom: "0", - }, -}); - -function formatDate(date: Date | string) { - if (date === "N/A") return date; - return new Date(date).toLocaleDateString("en-UK"); -} - -interface SubmissionsTableProps { - applications: SubmissionData[]; -} - -const SubmissionsTable: React.FC = ({ - applications, -}) => { - return ( - - - - - {[ - "Session ID", - "Submitted At", - "Payment Requests", - "Payment Status", - "BOPS Applications", - "Uniform Applications", - "Email Applications", - ].map((headCell) => ( - {headCell} - ))} - - - - {applications.map((row) => ( - - {row.sessionId} - {formatDate(row.submittedAt)} - - {row.paymentRequests && row.paymentRequests.length > 0 - ? row.paymentRequests.map((request, index) => ( - - ID: {request.id}, Created At:{" "} - {formatDate(request.createdAt)}, Paid At:{" "} - {formatDate(request.paidAt)}, Gov Payment ID:{" "} - {request.govPaymentId} - - )) - : "None"} - - - {row.paymentStatus && row.paymentStatus.length > 0 - ? row.paymentStatus.map((status, index) => ( - - Gov Payment ID: {status.govPaymentId}, Created At:{" "} - {formatDate(status.createdAt)}, Status: {status.status} - - )) - : "None"} - - - {row.bopsApplications && row.bopsApplications.length > 0 - ? row.bopsApplications.map((app, index) => ( - - ID: {app.id}, Submitted At:{" "} - {formatDate(app.submittedAt)}, Destination URL:{" "} - {app.destinationUrl} - - )) - : "None"} - - - {row.uniformApplications && row.uniformApplications.length > 0 - ? row.uniformApplications.map((app, index) => ( - - ID: {app.id}, Submitted At:{" "} - {formatDate(app.submittedAt)} - - )) - : "None"} - - - {row.emailApplications && row.emailApplications.length > 0 - ? row.emailApplications.map((email, index) => ( - - ID: {email.id}, Recipient: {email.recipient}, Submitted - At: {formatDate(email.submittedAt)} - - )) - : "None"} - - - ))} - - - - ); -}; - -export default SubmissionsTable; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/SubmissionsView.test.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/SubmissionsView.test.tsx deleted file mode 100644 index a812d4c953..0000000000 --- a/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/SubmissionsView.test.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { waitFor } from "@storybook/testing-library"; -import React from "react"; -import { axe, setup } from "testUtils"; - -import { mockApplications } from "./mocks"; -import SubmissionsView from "./SubmissionsView"; - -describe("SubmissionsView Component", () => { - test("displays the loading indicator when loading", async () => { - const { container, getByTestId } = setup( - , - ); - await waitFor(() => { - expect(getByTestId("delayed-loading-indicator")).toBeInTheDocument(); - }); - const results = await axe(container); - expect(results).toHaveNoViolations(); - }); - - test("displays the error message when there is an error", async () => { - const errorMessage = "Test error message"; - const { container, getByText } = setup( - , - ); - expect(getByText(errorMessage)).toBeInTheDocument(); - const results = await axe(container); - expect(results).toHaveNoViolations(); - }); - - test("displays a message when there are no applications", async () => { - const { container, getByText } = setup( - , - ); - expect( - getByText("No submitted applications found for this service."), - ).toBeInTheDocument(); - - const results = await axe(container); - expect(results).toHaveNoViolations(); - }); - - test("displays the submissions table when there are applications", async () => { - const { container, getByText } = setup( - , - ); - mockApplications.forEach((app) => { - expect(getByText(app.sessionId)).toBeInTheDocument(); - }); - - const results = await axe(container); - expect(results).toHaveNoViolations(); - }); -}); diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/SubmissionsView.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/SubmissionsView.tsx deleted file mode 100644 index b6be849c31..0000000000 --- a/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/SubmissionsView.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import Typography from "@mui/material/Typography"; -import DelayedLoadingIndicator from "components/DelayedLoadingIndicator"; -import ErrorFallback from "components/ErrorFallback"; -import React from "react"; - -import { SubmissionData } from "./submissionData"; -import SubmissionsTable from "./SubmissionsTable"; - -interface SubmissionsViewProps { - applications: SubmissionData[]; - loading: boolean; - error: Error | undefined; -} - -const SubmissionsView: React.FC = ({ - applications, - loading, - error, -}) => { - if (loading) return ; - if (error) return ; - if (applications.length === 0) - return ( - - No submitted applications found for this service. - - ); - return ; -}; - -export default SubmissionsView; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/index.stories.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/index.stories.tsx deleted file mode 100644 index ba082f27ca..0000000000 --- a/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/index.stories.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { MockedProvider } from "@apollo/client/testing"; -import { Meta, StoryObj } from "@storybook/react"; -import { vanillaStore } from "pages/FlowEditor/lib/store"; -import React from "react"; - -import Submissions from "./index"; -import { mockRequests } from "./mocks"; - -const { setState } = vanillaStore; -setState({ flowSlug: "test-service", teamSlug: "test-team" }); - -const meta: Meta = { - title: "Design System/Molecules/Submissions", - component: Submissions, -}; - -type Story = StoryObj; - -export default meta; - -export const DefaultView: Story = { - render: () => ( - - - - ), -}; - -export const LoadingState: Story = { - render: () => ( - - - - ), -}; - -export const ErrorState: Story = { - render: () => ( - - - - ), -}; - -export const NoResults: Story = { - render: () => ( - - - - ), -}; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/index.test.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/index.test.tsx deleted file mode 100644 index c5d514a036..0000000000 --- a/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/index.test.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { MockedProvider } from "@apollo/client/testing"; -import { waitFor } from "@testing-library/react"; -import { vanillaStore } from "pages/FlowEditor/lib/store"; -import React from "react"; -import { axe, setup } from "testUtils"; - -import Submissions from "./index"; -import { mockApplications, mockRequests } from "./mocks"; - -const { setState } = vanillaStore; - -describe("Submissions Component", () => { - test("no results message", async () => { - setState({ flowSlug: "no-results-service", teamSlug: "test-team" }); - const { container, getByText } = setup( - - - , - ); - - expect(getByText("Submissions")).toBeInTheDocument(); - expect( - getByText( - "View data on the user submitted applications for this service.", - ), - ).toBeInTheDocument(); - - await waitFor(() => { - expect( - getByText("No submitted applications found for this service."), - ).toBeInTheDocument(); - }); - - const results = await axe(container); - expect(results).toHaveNoViolations(); - }); - - test("basic view table with expected values", async () => { - setState({ flowSlug: "test-service", teamSlug: "test-team" }); - const { container, getByText } = setup( - - - , - ); - - expect(getByText("Submissions")).toBeInTheDocument(); - expect( - getByText( - "View data on the user submitted applications for this service.", - ), - ).toBeInTheDocument(); - - await waitFor(() => { - mockApplications.forEach((app) => { - expect(getByText(app.sessionId)).toBeInTheDocument(); - }); - }); - - const results = await axe(container); - expect(results).toHaveNoViolations(); - }); - - test("error renders as expected", async () => { - setState({ flowSlug: "error-service", teamSlug: "test-team" }); - const { container, getByText } = setup( - - - , - ); - - expect(getByText("Submissions")).toBeInTheDocument(); - expect( - getByText( - "View data on the user submitted applications for this service.", - ), - ).toBeInTheDocument(); - - await waitFor(() => { - expect(getByText("An error occurred")).toBeInTheDocument(); - }); - - const results = await axe(container); - expect(results).toHaveNoViolations(); - }); -}); diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/index.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/index.tsx index 18a274af85..b7b3b964d3 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/index.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/index.tsx @@ -1,40 +1,97 @@ +import { gql, useQuery } from "@apollo/client"; import Box from "@mui/material/Box"; +import Container from "@mui/material/Container"; import Typography from "@mui/material/Typography"; -import React from "react"; +import React, { useMemo } from "react"; import EditorRow from "ui/editor/EditorRow"; import { useStore } from "../../../lib/store"; -import { useSubmittedApplications } from "./submissionData"; -import SubmissionsView from "./SubmissionsView"; +import EventsLog from "./EventsLog"; + +export interface Submission { + sessionId: string; + eventId: string; + eventType: + | "Pay" + | "Submit to BOPS" + | "Submit to Uniform" + | "Send to email" + | "Upload to AWS S3"; + status?: + | "Success" + | "Failed (500)" // Hasura scheduled event status codes + | "Failed (502)" + | "Failed (503)" + | "Failed (504)" + | "Failed (400)" + | "Failed (401)" + | "Started" // Payment status enum codes (excluding "Created") + | "Submitted" + | "Capturable" + | "Failed" + | "Cancelled" + | "Error" + | "Unknown"; + retry: boolean; + response: Record; + createdAt: string; +} + +export interface GetSubmissionsResponse { + submissions: Submission[]; + loading: boolean; + error: Error | undefined; +} const Submissions: React.FC = () => { - const [flowSlug, teamSlug] = useStore((state) => [ - state.flowSlug, - state.teamSlug, - ]); - const { applications, loading, error } = useSubmittedApplications({ - flowSlug, - teamSlug, - }); + const flowId = useStore((state) => state.id); + + // submission_services_log view is already filtered for events >= Jan 1 2024 + const { data, loading, error } = useQuery<{ submissions: Submission[] }>( + gql` + query GetSubmissions($flow_id: uuid!) { + submissions: submission_services_log( + where: { flow_id: { _eq: $flow_id } } + order_by: { created_at: asc } + ) { + sessionId: session_id + eventId: event_id + eventType: event_type + status: status + retry: retry + response: response + createdAt: created_at + } + } + `, + { + variables: { flow_id: flowId }, + skip: !flowId, + }, + ); + + const submissions = useMemo(() => data?.submissions || [], [data]); return ( - - - - Submissions - - - View data on the user submitted applications for this service. - - - - - - + + + + + Submissions + + + Feed of payment and submission events for this service + + + + + + + ); }; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/mocks.ts b/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/mocks.ts deleted file mode 100644 index cc9cdac3a3..0000000000 --- a/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/mocks.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { SUBMITTED_APPLICATIONS_QUERY } from "./submissionData"; - -export const mockApplications = [ - { - sessionId: "test-session-3", - submittedAt: "2024-03-22T12:00:00.000Z", - paymentRequests: null, - paymentStatus: [ - { - govPaymentId: "test-govpay-3", - createdAt: "2024-03-22T11:50:00.000Z", - status: "error", - }, - { - govPaymentId: "test-govpay-3", - createdAt: "2024-03-22T11:49:00.000Z", - status: "created", - }, - ], - bopsApplications: null, - uniformApplications: [ - { - id: "test-uniform-2", - submittedAt: "2024-03-22T11:55:00.000Z", - }, - ], - emailApplications: [ - { - id: 3, - recipient: "test-user-3@opensystemslab.io", - submittedAt: "2024-03-22T12:00:00.000Z", - }, - ], - }, - - { - sessionId: "test-session-2", - submittedAt: "2024-03-21T11:30:00.000Z", - paymentRequests: [ - { - id: "test-payment-request-2", - createdAt: "2024-03-21T11:20:00.000Z", - paidAt: "2024-03-21T11:25:00.000Z", - govPaymentId: "test-govpay-2", - }, - ], - paymentStatus: [ - { - govPaymentId: "test-govpay-2", - createdAt: "2024-03-21T11:25:00.001Z", - status: "success", - }, - { - govPaymentId: "test-govpay-2", - createdAt: "2024-03-21T11:24:00.001Z", - status: "created", - }, - ], - bopsApplications: null, - uniformApplications: null, - emailApplications: [ - { - id: 2, - recipient: "test-user-2@opensystemslab.io", - submittedAt: "2024-03-21T11:30:00.000Z", - }, - ], - }, - { - sessionId: "test-session-1", - submittedAt: "2024-03-20T10:00:00.000Z", - paymentRequests: [ - { - id: "test-payment-request-1", - createdAt: "2024-03-20T09:50:00.000Z", - paidAt: "2024-03-20T09:55:00.000Z", - govPaymentId: "test-govpay-1", - }, - ], - paymentStatus: [ - { - govPaymentId: "test-govpay-1", - createdAt: "2024-03-20T09:55:00.001Z", - status: "success", - }, - { - govPaymentId: "test-govpay-1", - createdAt: "2024-03-20T09:50:00.000Z", - status: "created", - }, - ], - bopsApplications: [ - { - id: "test-bops-1", - submittedAt: "2024-03-20T09:53:00.000Z", - destinationUrl: "https://test.opensystemslab.io/bops/1", - }, - ], - uniformApplications: [ - { - id: "test-uniform-1", - submittedAt: "2024-03-20T09:52:00.000Z", - }, - ], - emailApplications: [ - { - id: 1, - recipient: "test-user-1@opensystemslab.io", - submittedAt: "2024-03-20T10:00:00.000Z", - }, - ], - }, -]; - -export const mockRequests = [ - { - request: { - query: SUBMITTED_APPLICATIONS_QUERY, - variables: { service_slug: "test-service", team_slug: "test-team" }, - }, - result: { - data: { - submissionServicesSummary: mockApplications, - }, - }, - }, - { - request: { - query: SUBMITTED_APPLICATIONS_QUERY, - variables: { service_slug: "no-results-service", team_slug: "test-team" }, - }, - result: { - data: { - submissionServicesSummary: [], - }, - }, - }, - { - request: { - query: SUBMITTED_APPLICATIONS_QUERY, - variables: { service_slug: "error-service", team_slug: "test-team" }, - }, - error: new Error("An error occurred"), - }, -]; diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/submissionData.ts b/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/submissionData.ts deleted file mode 100644 index 03b0b2295e..0000000000 --- a/editor.planx.uk/src/pages/FlowEditor/components/Settings/Submissions/submissionData.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { useQuery } from "@apollo/client"; -import gql from "graphql-tag"; -import { useMemo } from "react"; - -type PaymentRequest = { - id: string; - createdAt: string; - paidAt: string; - govPaymentId: string; -}; - -type PaymentStatus = { - govPaymentId: string; - createdAt: string; - status: string; -}; - -type BopsApplication = { - id: string; - submittedAt: string; - destinationUrl: string; -}; - -type EmailApplication = { - id: number; - recipient: string; - submittedAt: string; -}; - -type UniformApplication = { - id: string; - submittedAt: string; -}; - -export type SubmissionData = { - sessionId: string; - submittedAt: Date | string; - paymentRequests: PaymentRequest[] | null; - paymentStatus: PaymentStatus[] | null; - bopsApplications: BopsApplication[] | null; - uniformApplications: UniformApplication[] | null; - emailApplications: EmailApplication[] | null; -}; - -export type SubmittedApplicationsQueryResult = { - submissionServicesSummary: SubmissionData[]; -}; - -export const SUBMITTED_APPLICATIONS_QUERY = gql` - query SubmittedApplications($service_slug: String!, $team_slug: String!) { - submissionServicesSummary: submission_services_summary( - where: { - service_slug: { _eq: $service_slug } - team_slug: { _eq: $team_slug } - submitted_at: { _is_null: false } - } - order_by: { submitted_at: desc } - ) { - sessionId: session_id - submittedAt: submitted_at - paymentRequests: payment_requests - paymentStatus: payment_status - bopsApplications: bops_applications - uniformApplications: uniform_applications - emailApplications: email_applications - } - } -`; - -type UseSubmittedApplicationsParams = { - flowSlug?: string; - teamSlug?: string; -}; - -export const useSubmittedApplications = ({ - flowSlug, - teamSlug, -}: UseSubmittedApplicationsParams) => { - const { data, loading, error } = useQuery( - SUBMITTED_APPLICATIONS_QUERY, - { - variables: { service_slug: flowSlug, team_slug: teamSlug }, - skip: !flowSlug || !teamSlug, - }, - ); - - const applications = useMemo( - () => data?.submissionServicesSummary || [], - [data], - ); - - return { applications, loading, error }; -}; diff --git a/hasura.planx.uk/metadata/tables.yaml b/hasura.planx.uk/metadata/tables.yaml index d66aacabf7..aa49202e93 100644 --- a/hasura.planx.uk/metadata/tables.yaml +++ b/hasura.planx.uk/metadata/tables.yaml @@ -1434,6 +1434,33 @@ - table: name: submission_services_log schema: public + select_permissions: + - role: platformAdmin + permission: + columns: + - retry + - response + - event_id + - event_type + - status + - created_at + - flow_id + - session_id + filter: {} + comment: "" + - role: teamEditor + permission: + columns: + - retry + - response + - event_id + - event_type + - status + - created_at + - flow_id + - session_id + filter: {} + comment: "" - table: name: submission_services_summary schema: public diff --git a/hasura.planx.uk/migrations/1715874150959_update submission services log/down.sql b/hasura.planx.uk/migrations/1715874150959_update submission services log/down.sql new file mode 100644 index 0000000000..fe0efbb9db --- /dev/null +++ b/hasura.planx.uk/migrations/1715874150959_update submission services log/down.sql @@ -0,0 +1,45 @@ +create or replace view public.submission_services_log as +with payments as ( + select + session_id, + payment_id::text as event_id, + 'Pay' as event_type, + initcap(status) as status, + '{}'::jsonb as response, + created_at + from payment_status + where status != 'created' + and created_at >= '2024-01-01' +), submissions as ( + select + (seil.request -> 'payload' -> 'payload' ->> 'sessionId')::uuid as session_id, + se.id as event_id, + case + when se.webhook_conf::text like '%bops%' then 'Submit to BOPS' + when se.webhook_conf::text like '%uniform%' then 'Submit to Uniform' + when se.webhook_conf::text like '%email-submission%' then 'Send to email' + when se.webhook_conf::text like '%upload-submission%' then 'Upload to AWS S3' + else se.webhook_conf::text + end as event_type, + case + when seil.status = 200 then 'Success' + else format('Failed (%s)', seil.status) + end as status, + seil.response::jsonb, + seil.created_at + from hdb_catalog.hdb_scheduled_events se + left join hdb_catalog.hdb_scheduled_event_invocation_logs seil on seil.event_id = se.id + where se.webhook_conf::text not like '%email/%' + and seil.created_at >= '2024-01-01' +), all_events as ( + select * from payments + union all + select * from submissions +) +SELECT + ls.flow_id, + ae.* +FROM all_events ae + left join public.lowcal_sessions ls on ls.id = ae.session_id +WHERE ls.flow_id is not null +order by ae.created_at desc; diff --git a/hasura.planx.uk/migrations/1715874150959_update submission services log/up.sql b/hasura.planx.uk/migrations/1715874150959_update submission services log/up.sql new file mode 100644 index 0000000000..8c007c8eca --- /dev/null +++ b/hasura.planx.uk/migrations/1715874150959_update submission services log/up.sql @@ -0,0 +1,65 @@ +create or replace view public.submission_services_log as +with payments as ( + select + ps.session_id, + ps.payment_id::text as event_id, + 'Pay' as event_type, + initcap(ps.status) as status, + jsonb_build_object( + 'status', ps.status, + 'description', pse.comment, + 'govuk_pay_reference', ps.payment_id::text + ) as response, + ps.created_at, + false as retry + from payment_status ps + left join payment_status_enum pse on pse.value = ps.status + where ps.status != 'created' + and ps.created_at >= '2024-01-01' +), retries as ( + select + id + from hdb_catalog.hdb_scheduled_event_invocation_logs + where (event_id, created_at) in ( + select + seil.event_id, + max(seil.created_at) + from hdb_catalog.hdb_scheduled_event_invocation_logs seil + left join hdb_catalog.hdb_scheduled_events se on se.id = seil.event_id + where se.tries > 1 + group by seil.event_id + ) +), submissions as ( + select + (seil.request -> 'payload' -> 'payload' ->> 'sessionId')::uuid as session_id, + se.id as event_id, + case + when se.webhook_conf::text like '%bops%' then 'Submit to BOPS' + when se.webhook_conf::text like '%uniform%' then 'Submit to Uniform' + when se.webhook_conf::text like '%email-submission%' then 'Send to email' + when se.webhook_conf::text like '%upload-submission%' then 'Upload to AWS S3' + else se.webhook_conf::text + end as event_type, + case + when seil.status = 200 then 'Success' + else format('Failed (%s)', seil.status) + end as status, + seil.response::jsonb, + seil.created_at, + exists(select 1 from retries r where r.id = seil.id) as retry + from hdb_catalog.hdb_scheduled_events se + left join hdb_catalog.hdb_scheduled_event_invocation_logs seil on seil.event_id = se.id + where se.webhook_conf::text not like '%email/%' + and seil.created_at >= '2024-01-01' +), all_events as ( + select * from payments + union all + select * from submissions +) +SELECT + ls.flow_id, + ae.* +FROM all_events ae + left join public.lowcal_sessions ls on ls.id = ae.session_id +WHERE ls.flow_id is not null +order by ae.created_at desc;