From be796f121ca0d9e1daafd5c19d38cdec12a19aa4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= <DafyddLlyr@gmail.com>
Date: Mon, 20 Feb 2023 15:05:32 +0000
Subject: [PATCH] feat: Send users confirmation when application submitted
 (#1468)

---
 .env.example                                 |  1 +
 api.planx.uk/.env.test.example               |  1 +
 api.planx.uk/auth/index.ts                   | 12 ++---
 api.planx.uk/saveAndReturn/sendEmail.test.ts | 10 ++--
 api.planx.uk/saveAndReturn/utils.ts          | 49 ++++++++++++++------
 docker-compose.yml                           |  1 +
 hasura.planx.uk/metadata/tables.yaml         | 29 ++++++++++++
 infrastructure/application/index.ts          |  4 ++
 8 files changed, 82 insertions(+), 25 deletions(-)

diff --git a/.env.example b/.env.example
index f5cd10abfb..47b3cf616e 100644
--- a/.env.example
+++ b/.env.example
@@ -70,6 +70,7 @@ GOVUK_NOTIFY_RESUME_EMAIL_TEMPLATE_ID=👻
 GOVUK_NOTIFY_REMINDER_EMAIL_TEMPLATE_ID=👻
 GOVUK_NOTIFY_EXPIRY_EMAIL_TEMPLATE_ID=👻
 GOVUK_NOTIFY_SUBMISSION_EMAIL_TEMPLATE_ID=👻
+GOVUK_NOTIFY_CONFIRMATION_EMAIL_TEMPLATE_ID=👻
 
 UNIFORM_TOKEN_URL=👻
 UNIFORM_SUBMISSION_URL=👻
diff --git a/api.planx.uk/.env.test.example b/api.planx.uk/.env.test.example
index 35dd3c3475..9b12f41504 100644
--- a/api.planx.uk/.env.test.example
+++ b/api.planx.uk/.env.test.example
@@ -37,6 +37,7 @@ GOVUK_NOTIFY_RESUME_EMAIL_TEMPLATE_ID=👻
 GOVUK_NOTIFY_REMINDER_EMAIL_TEMPLATE_ID=👻
 GOVUK_NOTIFY_EXPIRY_EMAIL_TEMPLATE_ID=👻
 GOVUK_NOTIFY_SUBMISSION_EMAIL_TEMPLATE_ID=👻
+GOVUK_NOTIFY_CONFIRMATION_EMAIL_TEMPLATE_ID=👻
 
 UNIFORM_TOKEN_URL=👻
 UNIFORM_SUBMISSION_URL=👻
diff --git a/api.planx.uk/auth/index.ts b/api.planx.uk/auth/index.ts
index 8d6b36aeef..4a8b0fc651 100644
--- a/api.planx.uk/auth/index.ts
+++ b/api.planx.uk/auth/index.ts
@@ -1,6 +1,5 @@
 import { Request, Response, NextFunction } from "express";
 import crypto from "crypto";
-import { singleSessionEmailTemplates } from "../saveAndReturn/utils";
 import assert from "assert";
 
 /**
@@ -39,21 +38,18 @@ const useSendEmailAuth = (
   next: NextFunction
 ): void => {
   switch (req.params.template) {
+    // Requires authorization - can only be triggered by Hasura scheduled events
     case "reminder":
     case "expiry":
-      // Requires authorization - can only be triggered by Hasura scheduled events
+    case "confirmation":
       return useHasuraAuth(req, res, next);
+    // Public access
     case "save":
-      // Public access
       return next();
     default: {
-      // Invalid template
-      const validTemplates = Object.keys(singleSessionEmailTemplates);
       return next({
         status: 400,
-        message: `Invalid template - must be one of [${validTemplates.join(
-          ", "
-        )}]`,
+        message: "Invalid template",
       });
     }
   }
diff --git a/api.planx.uk/saveAndReturn/sendEmail.test.ts b/api.planx.uk/saveAndReturn/sendEmail.test.ts
index 50164be7f0..09ea086007 100644
--- a/api.planx.uk/saveAndReturn/sendEmail.test.ts
+++ b/api.planx.uk/saveAndReturn/sendEmail.test.ts
@@ -127,14 +127,16 @@ describe("Send Email endpoint", () => {
         .send(data)
         .expect(400)
         .then(response => {
-          expect(response.body).toHaveProperty("error", 'Invalid template - must be one of [save, reminder, expiry, submit]');
+          expect(response.body).toHaveProperty("error", "Invalid template");
         });
     });
   });
 
   describe("Templates which require authorisation", () => {
+    const templates = ["reminder", "expiry", "confirmation"];
+
     it("returns 401 UNAUTHORIZED if no auth header is provided", async () => {
-      for (const template of ["reminder", "expiry"]) {
+      for (const template of templates) {
         const data = { payload: { sessionId: 123, email: TEST_EMAIL } };
         await supertest(app)
           .post(`/send-email/${template}`)
@@ -144,7 +146,7 @@ describe("Send Email endpoint", () => {
     });
 
     it("returns 401 UNAUTHORIZED if no incorrect auth header is provided", async () => {
-      for (const template of ["reminder", "expiry"]) {
+      for (const template of templates) {
         const data = { payload: { sessionId: 123, email: TEST_EMAIL } };
         await supertest(app)
           .post(`/send-email/${template}`)
@@ -155,7 +157,7 @@ describe("Send Email endpoint", () => {
     });
 
     it("returns 200 OK if the correct headers are used", async () => {
-      for (const template of ["reminder", "expiry"]) {
+      for (const template of templates) {
         const data = { payload: { sessionId: 123, email: TEST_EMAIL } };
         await supertest(app)
           .post(`/send-email/${template}`)
diff --git a/api.planx.uk/saveAndReturn/utils.ts b/api.planx.uk/saveAndReturn/utils.ts
index cf86d3602a..45624e869e 100644
--- a/api.planx.uk/saveAndReturn/utils.ts
+++ b/api.planx.uk/saveAndReturn/utils.ts
@@ -1,25 +1,42 @@
 import { format, addDays } from "date-fns";
-import { gql } from "graphql-request";
+import { gql, GraphQLClient } from "graphql-request";
 import { publicGraphQLClient as publicClient, adminGraphQLClient as adminClient } from "../hasura";
 import { EmailSubmissionNotifyConfig, LowCalSession, SaveAndReturnNotifyConfig, Team } from "../types";
 import { notifyClient } from "./notify";
 
 const DAYS_UNTIL_EXPIRY = 28;
 
-const singleSessionEmailTemplates = {
+/**
+ * Triggered by applicants when saving
+ * Validated using email address & sessionId
+ */
+const publicEmailTemplates = {
   save: process.env.GOVUK_NOTIFY_SAVE_RETURN_EMAIL_TEMPLATE_ID,
-  reminder: process.env.GOVUK_NOTIFY_REMINDER_EMAIL_TEMPLATE_ID,
-  expiry: process.env.GOVUK_NOTIFY_EXPIRY_EMAIL_TEMPLATE_ID,
-  submit: process.env.GOVUK_NOTIFY_SUBMISSION_EMAIL_TEMPLATE_ID,
 };
 
-const multipleSessionEmailTemplates = {
+/**
+ * Triggered by applicants when resuming
+ * Validated using email address & inbox (magic link)
+ */
+const hybridEmailTemplates = {
   resume: process.env.GOVUK_NOTIFY_RESUME_EMAIL_TEMPLATE_ID,
 };
 
+/**
+ * Triggered by Hasura scheduled events
+ * Validated with the useHasuraAuth() middleware
+ */
+const privateEmailTemplates = {
+  reminder: process.env.GOVUK_NOTIFY_REMINDER_EMAIL_TEMPLATE_ID,
+  expiry: process.env.GOVUK_NOTIFY_EXPIRY_EMAIL_TEMPLATE_ID,
+  confirmation: process.env.GOVUK_NOTIFY_CONFIRMATION_EMAIL_TEMPLATE_ID,
+  submit: process.env.GOVUK_NOTIFY_SUBMISSION_EMAIL_TEMPLATE_ID,
+};
+
 const emailTemplates = {
-  ...singleSessionEmailTemplates,
-  ...multipleSessionEmailTemplates,
+  ...publicEmailTemplates,
+  ...hybridEmailTemplates,
+  ...privateEmailTemplates,
 };
 
 export type Template = keyof typeof emailTemplates;
@@ -94,7 +111,7 @@ const calculateExpiryDate = (createdAt: string): string => {
 };
 
 /**
- * Sends "Save", "Remind", and "Expiry" emails to Save & Return users
+ * Sends "Save", "Remind", "Expiry" and "Confirmation" emails to Save & Return users
  */
 const sendSingleApplicationEmail = async (
   template: Template,
@@ -104,7 +121,8 @@ const sendSingleApplicationEmail = async (
   try {
     const { flowSlug, team, session } = await validateSingleSessionRequest(
       email,
-      sessionId
+      sessionId,
+      template,
     );
     const config = {
       personalisation: getPersonalisation(session, flowSlug, team),
@@ -125,7 +143,8 @@ const sendSingleApplicationEmail = async (
  */
 const validateSingleSessionRequest = async (
   email: string,
-  sessionId: string
+  sessionId: string,
+  template: Template,
 ) => {
   try {
     const query = gql`
@@ -147,10 +166,11 @@ const validateSingleSessionRequest = async (
         }
       }
     `;
+    const client = getClientForTemplate(template);
     const headers = getSaveAndReturnPublicHeaders(sessionId, email);
     const {
       lowcal_sessions: [session],
-    } = await publicClient.request(query, null, headers);
+    } = await client.request(query, null, headers);
 
     if (!session) throw Error(`Unable to find session: ${sessionId}`);
 
@@ -164,6 +184,10 @@ const validateSingleSessionRequest = async (
   }
 };
 
+const getClientForTemplate = (template: Template): GraphQLClient => (
+  template in privateEmailTemplates ? adminClient : publicClient
+);
+
 interface SessionDetails {
   hasUserSaved: boolean;
   address: any;
@@ -349,7 +373,6 @@ export {
   convertSlugToName,
   getResumeLink,
   sendSingleApplicationEmail,
-  singleSessionEmailTemplates,
   markSessionAsSubmitted,
   DAYS_UNTIL_EXPIRY,
   calculateExpiryDate,
diff --git a/docker-compose.yml b/docker-compose.yml
index cd4deda6e8..d6e64124bc 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -163,6 +163,7 @@ services:
       GOVUK_NOTIFY_REMINDER_EMAIL_TEMPLATE_ID: ${GOVUK_NOTIFY_REMINDER_EMAIL_TEMPLATE_ID}
       GOVUK_NOTIFY_EXPIRY_EMAIL_TEMPLATE_ID: ${GOVUK_NOTIFY_EXPIRY_EMAIL_TEMPLATE_ID}
       GOVUK_NOTIFY_SUBMISSION_EMAIL_TEMPLATE_ID: ${GOVUK_NOTIFY_SUBMISSION_EMAIL_TEMPLATE_ID}
+      GOVUK_NOTIFY_CONFIRMATION_EMAIL_TEMPLATE_ID: ${GOVUK_NOTIFY_CONFIRMATION_EMAIL_TEMPLATE_ID}
       HASURA_PLANX_API_KEY: ${HASURA_PLANX_API_KEY}
       FILE_API_KEY: ${FILE_API_KEY}
       SLACK_WEBHOOK_URL: ${SLACK_WEBHOOK_URL}
diff --git a/hasura.planx.uk/metadata/tables.yaml b/hasura.planx.uk/metadata/tables.yaml
index d5dc77e53a..f7e2a447d1 100644
--- a/hasura.planx.uk/metadata/tables.yaml
+++ b/hasura.planx.uk/metadata/tables.yaml
@@ -224,6 +224,35 @@
                 _is_null: true
         check: {}
   event_triggers:
+    - name: email_user_submission_confirmation
+      definition:
+        enable_manual: false
+        update:
+          columns:
+            - submitted_at
+      retry_conf:
+        num_retries: 0
+        interval_sec: 10
+        timeout_sec: 60
+      webhook_from_env: HASURA_PLANX_API_URL
+      headers:
+        - name: authorization
+          value_from_env: HASURA_PLANX_API_KEY
+      request_transform:
+        body:
+          action: transform
+          template: |-
+            {
+              "payload": {
+                "sessionId": {{$body.event.data.new.id}},
+                "email": {{$body.event.data.new.email}}
+              }
+            }
+        url: '{{$base_url}}/send-email/confirmation'
+        method: POST
+        version: 2
+        query_params: {}
+        template_engine: Kriti
     - name: setup_lowcal_expiry_events
       definition:
         enable_manual: false
diff --git a/infrastructure/application/index.ts b/infrastructure/application/index.ts
index 187a8d07ba..e7dfb34de6 100644
--- a/infrastructure/application/index.ts
+++ b/infrastructure/application/index.ts
@@ -394,6 +394,10 @@ export = async () => {
             name: "GOVUK_NOTIFY_SUBMISSION_EMAIL_TEMPLATE_ID",
             value: "7e77bdae-7379-4dd8-a8cc-086a0029163c",
           },
+          {
+            name: "GOVUK_NOTIFY_CONFIRMATION_EMAIL_TEMPLATE_ID",
+            value: "8b82b606-defa-4daa-8fdb-e78b852b8ffb",
+          },
           {
             name: "SLACK_WEBHOOK_URL",
             value: config.require("slack-webhook-url"),