Skip to content

Commit

Permalink
feat: Send users confirmation when application submitted (#1468)
Browse files Browse the repository at this point in the history
  • Loading branch information
DafyddLlyr authored Feb 20, 2023
1 parent 42f4dd4 commit be796f1
Show file tree
Hide file tree
Showing 8 changed files with 82 additions and 25 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=👻
Expand Down
1 change: 1 addition & 0 deletions api.planx.uk/.env.test.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=👻
Expand Down
12 changes: 4 additions & 8 deletions api.planx.uk/auth/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Request, Response, NextFunction } from "express";
import crypto from "crypto";
import { singleSessionEmailTemplates } from "../saveAndReturn/utils";
import assert from "assert";

/**
Expand Down Expand Up @@ -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",
});
}
}
Expand Down
10 changes: 6 additions & 4 deletions api.planx.uk/saveAndReturn/sendEmail.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`)
Expand All @@ -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}`)
Expand All @@ -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}`)
Expand Down
49 changes: 36 additions & 13 deletions api.planx.uk/saveAndReturn/utils.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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),
Expand All @@ -125,7 +143,8 @@ const sendSingleApplicationEmail = async (
*/
const validateSingleSessionRequest = async (
email: string,
sessionId: string
sessionId: string,
template: Template,
) => {
try {
const query = gql`
Expand All @@ -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}`);

Expand All @@ -164,6 +184,10 @@ const validateSingleSessionRequest = async (
}
};

const getClientForTemplate = (template: Template): GraphQLClient => (
template in privateEmailTemplates ? adminClient : publicClient
);

interface SessionDetails {
hasUserSaved: boolean;
address: any;
Expand Down Expand Up @@ -349,7 +373,6 @@ export {
convertSlugToName,
getResumeLink,
sendSingleApplicationEmail,
singleSessionEmailTemplates,
markSessionAsSubmitted,
DAYS_UNTIL_EXPIRY,
calculateExpiryDate,
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
29 changes: 29 additions & 0 deletions hasura.planx.uk/metadata/tables.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions infrastructure/application/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down

0 comments on commit be796f1

Please sign in to comment.