diff --git a/api.planx.uk/modules/webhooks/service/analyzeSessions/operations.ts b/api.planx.uk/modules/webhooks/service/analyzeSessions/operations.ts index 2b1e13b689..ca9ec158e2 100644 --- a/api.planx.uk/modules/webhooks/service/analyzeSessions/operations.ts +++ b/api.planx.uk/modules/webhooks/service/analyzeSessions/operations.ts @@ -12,12 +12,13 @@ import { Operation } from "../sanitiseApplicationData/types"; */ const ALLOW_LIST = [ - "proposal.projectType", "application.declaration.connection", - "property.type", "drawBoundary.action", - "user.role", + "findProperty.action", "property.constraints.planning", + "property.type", + "proposal.projectType", + "user.role", ]; export const getAnalyzeSessionOperations = (): Operation[] => [ diff --git a/editor.planx.uk/src/@planx/components/FindProperty/Public/Public.test.tsx b/editor.planx.uk/src/@planx/components/FindProperty/Public/Public.test.tsx index 5ce9bef4b8..343e6d7990 100644 --- a/editor.planx.uk/src/@planx/components/FindProperty/Public/Public.test.tsx +++ b/editor.planx.uk/src/@planx/components/FindProperty/Public/Public.test.tsx @@ -69,6 +69,7 @@ const osAddressProps = { "organisation-entity": "13", }, }, + "findProperty.action": "Selected an existing address", }; const proposedAddressProps = { @@ -117,6 +118,7 @@ const proposedAddressProps = { "organisation-entity": "13", }, }, + "findProperty.action": "Proposed a new address", }; jest.spyOn(SWR, "default").mockImplementation((url: any) => { diff --git a/editor.planx.uk/src/@planx/components/FindProperty/Public/index.tsx b/editor.planx.uk/src/@planx/components/FindProperty/Public/index.tsx index 762391b272..02307ff308 100644 --- a/editor.planx.uk/src/@planx/components/FindProperty/Public/index.tsx +++ b/editor.planx.uk/src/@planx/components/FindProperty/Public/index.tsx @@ -16,7 +16,12 @@ import ExternalPlanningSiteDialog, { DialogPurpose, } from "ui/public/ExternalPlanningSiteDialog"; -import { FindProperty, SiteAddress } from "../model"; +import { + FindProperty, + FindPropertyUserAction, + PASSPORT_COMPONENT_ACTION_KEY, + SiteAddress, +} from "../model"; import PickOSAddress from "./Autocomplete"; import PlotNewAddress from "./Map"; @@ -223,6 +228,11 @@ function Component(props: Props) { squareMetresToHectares(areaSquareMetres); } + newPassportData[PASSPORT_COMPONENT_ACTION_KEY] = + address?.source === "os" + ? FindPropertyUserAction.Existing + : FindPropertyUserAction.New; + props.handleSubmit?.({ data: { ...newPassportData } }); } }} diff --git a/editor.planx.uk/src/@planx/components/FindProperty/model.ts b/editor.planx.uk/src/@planx/components/FindProperty/model.ts index b9bf931e67..81b7d1f9b9 100644 --- a/editor.planx.uk/src/@planx/components/FindProperty/model.ts +++ b/editor.planx.uk/src/@planx/components/FindProperty/model.ts @@ -1,5 +1,12 @@ import { MoreInformation, parseMoreInformation } from "../shared"; +export enum FindPropertyUserAction { + Existing = "Selected an existing address", + New = "Proposed a new address", +} + +export const PASSPORT_COMPONENT_ACTION_KEY = "findProperty.action"; + export interface FindProperty extends MoreInformation { title: string; description: string; diff --git a/editor.planx.uk/src/pages/FlowEditor/lib/analytics/provider.tsx b/editor.planx.uk/src/pages/FlowEditor/lib/analytics/provider.tsx index 37dc35a4f4..5ba9bbf4ed 100644 --- a/editor.planx.uk/src/pages/FlowEditor/lib/analytics/provider.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/lib/analytics/provider.tsx @@ -37,12 +37,13 @@ import { * extract it into it's own column. */ export const ALLOW_LIST = [ - "proposal.projectType", "application.declaration.connection", - "property.type", "drawBoundary.action", - "user.role", + "findProperty.action", "property.constraints.planning", + "property.type", + "proposal.projectType", + "user.role", ] as const; let lastVisibleNodeAnalyticsLogId: number | undefined = undefined; diff --git a/hasura.planx.uk/migrations/1715340505747_add_find_property_action_analytics_views/down.sql b/hasura.planx.uk/migrations/1715340505747_add_find_property_action_analytics_views/down.sql new file mode 100644 index 0000000000..023b85d79b --- /dev/null +++ b/hasura.planx.uk/migrations/1715340505747_add_find_property_action_analytics_views/down.sql @@ -0,0 +1,124 @@ +CREATE OR REPLACE VIEW "public"."analytics_summary" AS + SELECT a.id AS analytics_id, + al.id AS analytics_log_id, + f.slug AS service_slug, + t.slug AS team_slug, + a.type AS analytics_type, + al.created_at AS analytics_log_created_at, + a.created_at AS analytics_created_at, + ((a.user_agent -> 'os'::text) ->> 'name'::text) AS operating_system, + ((a.user_agent -> 'browser'::text) ->> 'name'::text) AS browser, + ((a.user_agent -> 'platform'::text) ->> 'type'::text) AS platform, + a.referrer, + al.flow_direction, + (al.metadata ->> 'change'::text) AS change_metadata, + (al.metadata ->> 'back'::text) AS back_metadata, + (al.metadata ->> 'selectedUrls'::text) AS selected_urls, + (al.metadata ->> 'flag'::text) AS result_flag, + (al.metadata -> 'flagSet'::text) AS result_flagset, + ((al.metadata -> 'displayText'::text) ->> 'heading'::text) AS result_heading, + ((al.metadata -> 'displayText'::text) ->> 'description'::text) AS result_description, + (al.metadata -> 'helpTextUseful'::text) AS help_text_useful, + CASE + WHEN al.has_clicked_help THEN al.metadata + ELSE NULL::jsonb + END AS help_metadata, + al.user_exit AS is_user_exit, + al.node_type, + al.node_title, + al.has_clicked_help, + al.input_errors, + (date_part('epoch'::text, (al.next_log_created_at - al.created_at)))::numeric(10,1) AS time_spent_on_node_seconds, + a.ended_at AS analytics_ended_at, + ((date_part('epoch'::text, (a.ended_at - a.created_at)) / (60)::double precision))::numeric(10,1) AS time_spent_on_analytics_session_minutes, + al.node_id, + al.allow_list_answers, + (al.allow_list_answers -> 'proposal.projectType'::text) AS proposal_project_type, + (al.allow_list_answers -> 'application.declaration.connection'::text) AS application_declaration_connection, + (al.allow_list_answers -> 'property.type'::text) AS property_type, + (al.allow_list_answers -> 'drawBoundary.action'::text) AS draw_boundary_action, + (al.allow_list_answers -> 'user.role'::text) AS user_role, + (al.allow_list_answers -> 'property.constraints.planning'::text) AS property_constraints_planning + FROM (((analytics a + LEFT JOIN analytics_logs al ON ((a.id = al.analytics_id))) + LEFT JOIN flows f ON ((a.flow_id = f.id))) + LEFT JOIN teams t ON ((t.id = f.team_id))); + +CREATE OR REPLACE VIEW "public"."submission_services_summary" AS + WITH resumes_per_session AS ( + SELECT reconciliation_requests.session_id, + count(reconciliation_requests.id) AS number_times_resumed + FROM reconciliation_requests + GROUP BY reconciliation_requests.session_id + ), bops_agg AS ( + SELECT bops_applications.session_id, + json_agg(json_build_object('id', bops_applications.bops_id, 'submittedAt', bops_applications.created_at, 'destinationUrl', bops_applications.destination_url) ORDER BY bops_applications.created_at DESC) AS bops_applications + FROM bops_applications + GROUP BY bops_applications.session_id + ), email_agg AS ( + SELECT email_applications.session_id, + json_agg(json_build_object('id', email_applications.id, 'recipient', email_applications.recipient, 'submittedAt', email_applications.created_at) ORDER BY email_applications.created_at DESC) AS email_applications + FROM email_applications + GROUP BY email_applications.session_id + ), uniform_agg AS ( + SELECT uniform_applications.submission_reference, + json_agg(json_build_object('id', uniform_applications.idox_submission_id, 'submittedAt', uniform_applications.created_at) ORDER BY uniform_applications.created_at DESC) AS uniform_applications + FROM uniform_applications + GROUP BY uniform_applications.submission_reference + ), payment_requests_agg AS ( + SELECT payment_requests.session_id, + json_agg(json_build_object('id', payment_requests.id, 'createdAt', payment_requests.created_at, 'paidAt', payment_requests.paid_at, 'govpayPaymentId', payment_requests.govpay_payment_id) ORDER BY payment_requests.created_at DESC) AS payment_requests + FROM payment_requests + GROUP BY payment_requests.session_id + ), payment_status_agg AS ( + SELECT payment_status.session_id, + json_agg(json_build_object('govpayPaymentId', payment_status.payment_id, 'createdAt', payment_status.created_at, 'status', payment_status.status) ORDER BY payment_status.created_at DESC) AS payment_status + FROM payment_status + GROUP BY payment_status.session_id + ) + SELECT (ls.id)::text AS session_id, + t.slug AS team_slug, + f.slug AS service_slug, + ls.created_at, + ls.submitted_at, + ((ls.submitted_at)::date - (ls.created_at)::date) AS session_length_days, + ls.has_user_saved AS user_clicked_save, + rps.number_times_resumed, + ls.allow_list_answers, + (ls.allow_list_answers -> 'proposal.projectType'::text) AS proposal_project_type, + (ls.allow_list_answers -> 'application.declaration.connection'::text) AS application_declaration_connection, + (ls.allow_list_answers -> 'property.type'::text) AS property_type, + (ls.allow_list_answers -> 'drawBoundary.action'::text) AS draw_boundary_action, + (ls.allow_list_answers -> 'user.role'::text) AS user_role, + (ls.allow_list_answers -> 'property.constraints.planning'::text) AS property_constraints_planning, + CASE + WHEN (((pr.payment_requests)::jsonb IS NOT NULL) AND (jsonb_array_length((pr.payment_requests)::jsonb) > 0)) THEN true + ELSE false + END AS user_invited_to_pay, + pr.payment_requests, + ps.payment_status, + CASE + WHEN (((ba.bops_applications)::jsonb IS NOT NULL) AND (jsonb_array_length((ba.bops_applications)::jsonb) > 0)) THEN true + ELSE false + END AS sent_to_bops, + ba.bops_applications, + CASE + WHEN (((ua.uniform_applications)::jsonb IS NOT NULL) AND (jsonb_array_length((ua.uniform_applications)::jsonb) > 0)) THEN true + ELSE false + END AS sent_to_uniform, + ua.uniform_applications, + CASE + WHEN (((ea.email_applications)::jsonb IS NOT NULL) AND (jsonb_array_length((ea.email_applications)::jsonb) > 0)) THEN true + ELSE false + END AS sent_to_email, + ea.email_applications + FROM ((((((((lowcal_sessions ls + LEFT JOIN flows f ON ((f.id = ls.flow_id))) + LEFT JOIN teams t ON ((t.id = f.team_id))) + LEFT JOIN resumes_per_session rps ON ((rps.session_id = (ls.id)::text))) + LEFT JOIN payment_requests_agg pr ON ((pr.session_id = ls.id))) + LEFT JOIN payment_status_agg ps ON ((ps.session_id = ls.id))) + LEFT JOIN bops_agg ba ON ((ba.session_id = (ls.id)::text))) + LEFT JOIN uniform_agg ua ON ((ua.submission_reference = (ls.id)::text))) + LEFT JOIN email_agg ea ON ((ea.session_id = ls.id))) + WHERE ((f.slug IS NOT NULL) AND (t.slug IS NOT NULL)); diff --git a/hasura.planx.uk/migrations/1715340505747_add_find_property_action_analytics_views/up.sql b/hasura.planx.uk/migrations/1715340505747_add_find_property_action_analytics_views/up.sql new file mode 100644 index 0000000000..bbbff4b550 --- /dev/null +++ b/hasura.planx.uk/migrations/1715340505747_add_find_property_action_analytics_views/up.sql @@ -0,0 +1,126 @@ +CREATE OR REPLACE VIEW "public"."analytics_summary" AS + SELECT a.id AS analytics_id, + al.id AS analytics_log_id, + f.slug AS service_slug, + t.slug AS team_slug, + a.type AS analytics_type, + al.created_at AS analytics_log_created_at, + a.created_at AS analytics_created_at, + ((a.user_agent -> 'os'::text) ->> 'name'::text) AS operating_system, + ((a.user_agent -> 'browser'::text) ->> 'name'::text) AS browser, + ((a.user_agent -> 'platform'::text) ->> 'type'::text) AS platform, + a.referrer, + al.flow_direction, + (al.metadata ->> 'change'::text) AS change_metadata, + (al.metadata ->> 'back'::text) AS back_metadata, + (al.metadata ->> 'selectedUrls'::text) AS selected_urls, + (al.metadata ->> 'flag'::text) AS result_flag, + (al.metadata -> 'flagSet'::text) AS result_flagset, + ((al.metadata -> 'displayText'::text) ->> 'heading'::text) AS result_heading, + ((al.metadata -> 'displayText'::text) ->> 'description'::text) AS result_description, + (al.metadata -> 'helpTextUseful'::text) AS help_text_useful, + CASE + WHEN al.has_clicked_help THEN al.metadata + ELSE NULL::jsonb + END AS help_metadata, + al.user_exit AS is_user_exit, + al.node_type, + al.node_title, + al.has_clicked_help, + al.input_errors, + (date_part('epoch'::text, (al.next_log_created_at - al.created_at)))::numeric(10,1) AS time_spent_on_node_seconds, + a.ended_at AS analytics_ended_at, + ((date_part('epoch'::text, (a.ended_at - a.created_at)) / (60)::double precision))::numeric(10,1) AS time_spent_on_analytics_session_minutes, + al.node_id, + al.allow_list_answers, + (al.allow_list_answers -> 'proposal.projectType'::text) AS proposal_project_type, + (al.allow_list_answers -> 'application.declaration.connection'::text) AS application_declaration_connection, + (al.allow_list_answers -> 'property.type'::text) AS property_type, + (al.allow_list_answers -> 'drawBoundary.action'::text) AS draw_boundary_action, + (al.allow_list_answers -> 'user.role'::text) AS user_role, + (al.allow_list_answers -> 'property.constraints.planning'::text) AS property_constraints_planning, + (al.allow_list_answers -> 'findProperty.action'::text) AS find_property_action + FROM (((analytics a + LEFT JOIN analytics_logs al ON ((a.id = al.analytics_id))) + LEFT JOIN flows f ON ((a.flow_id = f.id))) + LEFT JOIN teams t ON ((t.id = f.team_id))); + +CREATE OR REPLACE VIEW "public"."submission_services_summary" AS + WITH resumes_per_session AS ( + SELECT reconciliation_requests.session_id, + count(reconciliation_requests.id) AS number_times_resumed + FROM reconciliation_requests + GROUP BY reconciliation_requests.session_id + ), bops_agg AS ( + SELECT bops_applications.session_id, + json_agg(json_build_object('id', bops_applications.bops_id, 'submittedAt', bops_applications.created_at, 'destinationUrl', bops_applications.destination_url) ORDER BY bops_applications.created_at DESC) AS bops_applications + FROM bops_applications + GROUP BY bops_applications.session_id + ), email_agg AS ( + SELECT email_applications.session_id, + json_agg(json_build_object('id', email_applications.id, 'recipient', email_applications.recipient, 'submittedAt', email_applications.created_at) ORDER BY email_applications.created_at DESC) AS email_applications + FROM email_applications + GROUP BY email_applications.session_id + ), uniform_agg AS ( + SELECT uniform_applications.submission_reference, + json_agg(json_build_object('id', uniform_applications.idox_submission_id, 'submittedAt', uniform_applications.created_at) ORDER BY uniform_applications.created_at DESC) AS uniform_applications + FROM uniform_applications + GROUP BY uniform_applications.submission_reference + ), payment_requests_agg AS ( + SELECT payment_requests.session_id, + json_agg(json_build_object('id', payment_requests.id, 'createdAt', payment_requests.created_at, 'paidAt', payment_requests.paid_at, 'govpayPaymentId', payment_requests.govpay_payment_id) ORDER BY payment_requests.created_at DESC) AS payment_requests + FROM payment_requests + GROUP BY payment_requests.session_id + ), payment_status_agg AS ( + SELECT payment_status.session_id, + json_agg(json_build_object('govpayPaymentId', payment_status.payment_id, 'createdAt', payment_status.created_at, 'status', payment_status.status) ORDER BY payment_status.created_at DESC) AS payment_status + FROM payment_status + GROUP BY payment_status.session_id + ) + SELECT (ls.id)::text AS session_id, + t.slug AS team_slug, + f.slug AS service_slug, + ls.created_at, + ls.submitted_at, + ((ls.submitted_at)::date - (ls.created_at)::date) AS session_length_days, + ls.has_user_saved AS user_clicked_save, + rps.number_times_resumed, + ls.allow_list_answers, + (ls.allow_list_answers -> 'proposal.projectType'::text) AS proposal_project_type, + (ls.allow_list_answers -> 'application.declaration.connection'::text) AS application_declaration_connection, + (ls.allow_list_answers -> 'property.type'::text) AS property_type, + (ls.allow_list_answers -> 'drawBoundary.action'::text) AS draw_boundary_action, + (ls.allow_list_answers -> 'user.role'::text) AS user_role, + (ls.allow_list_answers -> 'property.constraints.planning'::text) AS property_constraints_planning, + CASE + WHEN (((pr.payment_requests)::jsonb IS NOT NULL) AND (jsonb_array_length((pr.payment_requests)::jsonb) > 0)) THEN true + ELSE false + END AS user_invited_to_pay, + pr.payment_requests, + ps.payment_status, + CASE + WHEN (((ba.bops_applications)::jsonb IS NOT NULL) AND (jsonb_array_length((ba.bops_applications)::jsonb) > 0)) THEN true + ELSE false + END AS sent_to_bops, + ba.bops_applications, + CASE + WHEN (((ua.uniform_applications)::jsonb IS NOT NULL) AND (jsonb_array_length((ua.uniform_applications)::jsonb) > 0)) THEN true + ELSE false + END AS sent_to_uniform, + ua.uniform_applications, + CASE + WHEN (((ea.email_applications)::jsonb IS NOT NULL) AND (jsonb_array_length((ea.email_applications)::jsonb) > 0)) THEN true + ELSE false + END AS sent_to_email, + ea.email_applications, + (ls.allow_list_answers -> 'findProperty.action'::text) AS find_property_action + FROM ((((((((lowcal_sessions ls + LEFT JOIN flows f ON ((f.id = ls.flow_id))) + LEFT JOIN teams t ON ((t.id = f.team_id))) + LEFT JOIN resumes_per_session rps ON ((rps.session_id = (ls.id)::text))) + LEFT JOIN payment_requests_agg pr ON ((pr.session_id = ls.id))) + LEFT JOIN payment_status_agg ps ON ((ps.session_id = ls.id))) + LEFT JOIN bops_agg ba ON ((ba.session_id = (ls.id)::text))) + LEFT JOIN uniform_agg ua ON ((ua.submission_reference = (ls.id)::text))) + LEFT JOIN email_agg ea ON ((ea.session_id = ls.id))) + WHERE ((f.slug IS NOT NULL) AND (t.slug IS NOT NULL));