Skip to content

Commit

Permalink
feat: Send email docs (#2432)
Browse files Browse the repository at this point in the history
* chore: Move notify to lib

* chore: Move files to sendEmail module

* fix: Resolve circular dependency issue

* feat: Break into routes and controller

* test: Update test cases

* docs: Add Swagger docs
  • Loading branch information
DafyddLlyr authored Nov 17, 2023
1 parent 8ce1579 commit a083beb
Show file tree
Hide file tree
Showing 21 changed files with 555 additions and 379 deletions.
27 changes: 0 additions & 27 deletions api.planx.uk/inviteToPay/paymentRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,33 +128,6 @@ export const fetchPaymentRequestViaProxy = fetchPaymentViaProxyWithCallback(
},
);

export const addGovPayPaymentIdToPaymentRequest = async (
paymentRequestId: string,
govUKPayment: GovUKPayment,
): Promise<void> => {
const query = gql`
mutation AddGovPayPaymentIdToPaymentRequest(
$paymentRequestId: uuid!
$govPayPaymentId: String
) {
update_payment_requests_by_pk(
pk_columns: { id: $paymentRequestId }
_set: { govpay_payment_id: $govPayPaymentId }
) {
id
}
}
`;
try {
await $api.client.request(query, {
paymentRequestId,
govPayPaymentId: govUKPayment.payment_id,
});
} catch (error) {
throw Error(`payment request ${paymentRequestId} not updated`);
}
};

interface MarkPaymentRequestAsPaid {
updatePaymentRequestPaidAt: {
affectedRows: number;
Expand Down
12 changes: 7 additions & 5 deletions api.planx.uk/inviteToPay/sendConfirmationEmail.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import supertest from "supertest";
import app from "../server";
import { queryMock } from "../tests/graphqlQueryMock";
import { sendAgentAndPayeeConfirmationEmail } from "./sendConfirmationEmail";
import { sendEmail } from "../notify/notify";
import { sendEmail } from "../lib/notify";

jest.mock("../notify/notify", () => ({
jest.mock("../lib/notify", () => ({
sendEmail: jest.fn(),
}));

Expand Down Expand Up @@ -108,16 +108,18 @@ describe("Invite to pay confirmation templates cannot be sent individually", ()
test(`the "${template}" template`, async () => {
const data = {
payload: {
sessionId: "TestSesionID",
sessionId: "TestSessionID",
lockedAt: "2023-05-18T12:49:22.839068+00:00",
},
};
await supertest(app)
.post(`/send-email/${template}`)
.set("Authorization", "testtesttest")
.send(data)
.expect(400, {
error: `Failed to send "${template}" email. Invalid template`,
.expect(400)
.then((res) => {
expect(res.body).toHaveProperty("issues");
expect(res.body).toHaveProperty("name", "ZodError");
});
});
}
Expand Down
2 changes: 1 addition & 1 deletion api.planx.uk/inviteToPay/sendConfirmationEmail.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { $public, $api } from "../client";
import { sendEmail } from "../notify";
import { sendEmail } from "../lib/notify";
import { gql } from "graphql-request";
import { convertSlugToName } from "../modules/saveAndReturn/service/utils";
import type { AgentAndPayeeSubmissionNotifyConfig } from "../types";
Expand Down
6 changes: 2 additions & 4 deletions api.planx.uk/inviteToPay/sendPaymentEmail.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,8 @@ describe("Send email endpoint for invite to pay templates", () => {
.send(missingPaymentRequestId)
.expect(400)
.then((response) => {
expect(response.body).toHaveProperty(
"error",
`Failed to send "${template}" email. Required \`paymentRequestId\` missing`,
);
expect(response.body).toHaveProperty("issues");
expect(response.body).toHaveProperty("name", "ZodError");
});
});

Expand Down
2 changes: 1 addition & 1 deletion api.planx.uk/inviteToPay/sendPaymentEmail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
convertSlugToName,
getServiceLink,
} from "../modules/saveAndReturn/service/utils";
import { Template, getClientForTemplate, sendEmail } from "../notify";
import { Template, getClientForTemplate, sendEmail } from "../lib/notify";
import { InviteToPayNotifyConfig } from "../types";
import { Team } from "../types";
import type { PaymentRequest } from "@opensystemslab/planx-core/types";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { NotifyClient } from "notifications-node-client";
import { softDeleteSession } from "../modules/saveAndReturn/service/utils";
import { NotifyConfig } from "../types";
import { $api, $public } from "../client";
import { softDeleteSession } from "../../modules/saveAndReturn/service/utils";
import { NotifyConfig } from "../../types";
import { $api, $public } from "../../client";

const notifyClient = new NotifyClient(process.env.GOVUK_NOTIFY_API_KEY);

Expand Down
2 changes: 1 addition & 1 deletion api.planx.uk/modules/auth/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import crypto from "crypto";
import assert from "assert";
import { ServerError } from "../../errors";
import { Template } from "../../notify";
import { Template } from "../../lib/notify";
import { expressjwt } from "express-jwt";

import passport from "passport";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { gql } from "graphql-request";
import { LowCalSession, Team } from "../../../types";
import { convertSlugToName, getResumeLink, calculateExpiryDate } from "./utils";
import { sendEmail } from "../../../notify";
import { sendEmail } from "../../../lib/notify";
import type { SiteAddress } from "@opensystemslab/planx-core/types";
import { $api, $public } from "../../../client";

Expand Down
2 changes: 1 addition & 1 deletion api.planx.uk/modules/saveAndReturn/service/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { SiteAddress } from "@opensystemslab/planx-core/types";
import { format, addDays } from "date-fns";
import { gql } from "graphql-request";
import { LowCalSession, Team } from "../../../types";
import { Template, getClientForTemplate, sendEmail } from "../../../notify";
import { Template, getClientForTemplate, sendEmail } from "../../../lib/notify";
import { $api, $public } from "../../../client";

const DAYS_UNTIL_EXPIRY = 28;
Expand Down
88 changes: 88 additions & 0 deletions api.planx.uk/modules/sendEmail/controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {
sendSinglePaymentEmail,
sendAgentAndPayeeConfirmationEmail,
} from "../../inviteToPay";
import { sendSingleApplicationEmail } from "../saveAndReturn/service/utils";
import { ServerError } from "../../errors";
import { NextFunction } from "express";
import {
ConfirmationEmail,
PaymentEmail,
SingleApplicationEmail,
} from "./types";

export const singleApplicationEmailController: SingleApplicationEmail = async (
_req,
res,
next,
) => {
const { email, sessionId } = res.locals.parsedReq.body.payload;
const { template } = res.locals.parsedReq.params;

try {
const response = await sendSingleApplicationEmail({
template,
email,
sessionId,
});
return res.json(response);
} catch (error) {
emailErrorHandler(next, error, template);
}
};

export const paymentEmailController: PaymentEmail = async (_req, res, next) => {
const { paymentRequestId } = res.locals.parsedReq.body.payload;
const { template } = res.locals.parsedReq.params;

try {
const response = await sendSinglePaymentEmail({
template,
paymentRequestId,
});
return res.json(response);
} catch (error) {
emailErrorHandler(next, error, template);
}
};

export const confirmationEmailController: ConfirmationEmail = async (
_req,
res,
next,
) => {
const { lockedAt, sessionId, email } = res.locals.parsedReq.body.payload;
const { template } = res.locals.parsedReq.params;

try {
// if the session is locked we can infer that a payment request has been initiated
const paymentRequestInitiated = Boolean(lockedAt);
if (paymentRequestInitiated) {
const response = await sendAgentAndPayeeConfirmationEmail(sessionId);
return res.json(response);
} else {
const response = await sendSingleApplicationEmail({
template,
email,
sessionId,
});
return res.json(response);
}
} catch (error) {
emailErrorHandler(next, error, template);
}
};

const emailErrorHandler = (
next: NextFunction,
error: unknown,
template: string,
) =>
next(
new ServerError({
status: error instanceof ServerError ? error.status : undefined,
message: `Failed to send "${template}" email. ${
(error as Error).message
}`,
}),
);
95 changes: 95 additions & 0 deletions api.planx.uk/modules/sendEmail/docs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
openapi: 3.1.0
info:
title: Plan✕ API
version: 0.1.0
tags:
- name: send email
description: Send templated emails via the GovNotify service
components:
schemas:
SendEmailRequest:
type: object
properties:
payload:
oneOf:
- $ref: "#/components/schemas/SingleApplicationPayload"
- $ref: "#/components/schemas/PaymentPayload"
- $ref: "#/components/schemas/ConfirmationPayload"
SingleApplicationPayload:
type: object
properties:
email:
type: string
format: email
sessionId:
type: string
PaymentPayload:
type: object
properties:
paymentRequestId:
type: string
ConfirmationPayload:
type: object
properties:
sessionId:
type: string
lockedAt:
type: string
format: date-time
nullable: true
email:
type: string
format: email
responses:
SendEmailResponse:
type: object
properties:
message:
type: string
expiryDate:
type: string
format: date-time
nullable: true
paths:
/send-email/{template}:
post:
tags: [send email]
summary: Send an email
parameters:
- name: template
in: path
required: true
schema:
type: string
description: GovNotify template to use
enum:
[
"reminder",
"expiry",
"save",
"invite-to-pay",
"invite-to-pay-agent",
"payment-reminder",
"payment-reminder-agent",
"payment-expiry",
"payment-expiry-agent",
"confirmation",
]
requestBody:
description: |
Request body for sending email.
The structure varies based on the template.
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/SendEmailRequest"
responses:
"200":
description: Email sent successfully
content:
application/json:
schema:
$ref: "#/components/responses/SendEmailResponse"
"500":
$ref: "#/components/responses/ErrorMessage"
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import supertest from "supertest";
import app from "../server";
import { queryMock } from "../tests/graphqlQueryMock";
import app from "../../server";
import { queryMock } from "../../tests/graphqlQueryMock";
import {
mockFlow,
mockLowcalSession,
mockSetupEmailNotifications,
mockSoftDeleteLowcalSession,
mockValidateSingleSessionRequest,
} from "../tests/mocks/saveAndReturnMocks";
} from "../../tests/mocks/saveAndReturnMocks";
import { CoreDomainClient } from "@opensystemslab/planx-core";

// https://docs.notifications.service.gov.uk/node.html#email-addresses
Expand Down Expand Up @@ -39,10 +39,8 @@ describe("Send Email endpoint", () => {
.send(invalidBody)
.expect(400)
.then((response) => {
expect(response.body).toHaveProperty(
"error",
'Failed to send "save" email. Required value missing',
);
expect(response.body).toHaveProperty("issues");
expect(response.body).toHaveProperty("name", "ZodError");
});
}
});
Expand Down Expand Up @@ -75,9 +73,10 @@ describe("Send Email endpoint", () => {
await supertest(app)
.post(SAVE_ENDPOINT)
.send(data)
.expect(500)
.expect(400)
.then((response) => {
expect(response.body).toHaveProperty("error");
expect(response.body).toHaveProperty("issues");
expect(response.body).toHaveProperty("name", "ZodError");
});
});

Expand Down
Loading

0 comments on commit a083beb

Please sign in to comment.