Skip to content

Commit

Permalink
feat: Save and Return docs (#2426)
Browse files Browse the repository at this point in the history
  • Loading branch information
DafyddLlyr authored Nov 16, 2023
1 parent 7155fd1 commit 43e222f
Show file tree
Hide file tree
Showing 21 changed files with 357 additions and 169 deletions.
2 changes: 1 addition & 1 deletion api.planx.uk/inviteToPay/sendConfirmationEmail.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { $public, $api } from "../client";
import { sendEmail } from "../notify";
import { gql } from "graphql-request";
import { convertSlugToName } from "../saveAndReturn/utils";
import { convertSlugToName } from "../modules/saveAndReturn/service/utils";
import type { AgentAndPayeeSubmissionNotifyConfig } from "../types";

export async function sendAgentAndPayeeConfirmationEmail(sessionId: string) {
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 @@ -3,7 +3,7 @@ import {
calculateExpiryDate,
convertSlugToName,
getServiceLink,
} from "../saveAndReturn/utils";
} from "../modules/saveAndReturn/service/utils";
import { Template, getClientForTemplate, sendEmail } from "../notify";
import { InviteToPayNotifyConfig } from "../types";
import { Team } from "../types";
Expand Down
109 changes: 109 additions & 0 deletions api.planx.uk/modules/saveAndReturn/controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { z } from "zod";
import { ValidatedRequestHandler } from "../../shared/middleware/validate";
import { resumeApplication } from "./service/resumeApplication";
import { LowCalSessionData } from "../../types";
import { findSession, validateSession } from "./service/validateSession";
import { PaymentRequest } from "@opensystemslab/planx-core/types";

interface ResumeApplicationResponse {
message: string;
expiryDate?: string | undefined;
}

export const resumeApplicationSchema = z.object({
body: z.object({
payload: z.object({
teamSlug: z.string(),
email: z.string().email(),
}),
}),
});

export type ResumeApplication = ValidatedRequestHandler<
typeof resumeApplicationSchema,
ResumeApplicationResponse
>;

export const resumeApplicationController: ResumeApplication = async (
_req,
res,
next,
) => {
try {
const { teamSlug, email } = res.locals.parsedReq.body.payload;
const response = await resumeApplication(teamSlug, email);
return res.json(response);
} catch (error) {
return next({
error,
message: `Failed to send "Resume" email. ${(error as Error).message}`,
});
}
};

export interface ValidationResponse {
message: string;
changesFound: boolean | null;
alteredSectionIds?: Array<string>;
reconciledSessionData: Omit<LowCalSessionData, "passport">;
}

interface LockedSessionResponse {
message: "Session locked";
paymentRequest?: Partial<
Pick<PaymentRequest, "id" | "payeeEmail" | "payeeName">
>;
}

export const validateSessionSchema = z.object({
body: z.object({
payload: z.object({
sessionId: z.string(),
email: z.string().email(),
}),
}),
});

export type ValidateSessionController = ValidatedRequestHandler<
typeof validateSessionSchema,
ValidationResponse | LockedSessionResponse
>;

export const validateSessionController: ValidateSessionController = async (
_req,
res,
next,
) => {
try {
const { email, sessionId } = res.locals.parsedReq.body.payload;

const fetchedSession = await findSession({
sessionId,
email: email.toLowerCase(),
});

if (!fetchedSession) {
return next({
status: 404,
message: "Unable to find your session",
});
}

if (fetchedSession.lockedAt) {
return res.status(403).send({
message: "Session locked",
paymentRequest: {
...fetchedSession.paymentRequests?.[0],
},
});
}
const responseData = await validateSession(sessionId, fetchedSession);

return res.status(200).json(responseData);
} catch (error) {
return next({
error,
message: "Failed to validate session",
});
}
};
123 changes: 123 additions & 0 deletions api.planx.uk/modules/saveAndReturn/docs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
openapi: 3.1.0
info:
title: Plan✕ API
version: 0.1.0
tags:
- name: save and return
description: Endpoints used for "Save and Return" functionality
components:
schema:
ResumeApplication:
required: true
content:
application/json:
schema:
type: object
properties:
payload:
type: object
properties:
teamSlug:
type: string
email:
type: string
format: email
ValidateSession:
required: true
content:
application/json:
schema:
type: object
properties:
payload:
type: object
properties:
sessionId:
type: string
format: uuid
email:
type: string
format: email
responses:
ResumeApplication:
content:
application/json:
schema:
type: object
properties:
message:
required: true
type: string
expiryDate:
required: false
oneOf:
- type: string
- type: "null"
ValidationResponse:
type: object
properties:
message:
type: string
changesFound:
type: boolean
nullable: true
alteredSectionIds:
type: array
items:
type: string
reconciledSessionData:
type: object
properties:
breadcrumbs:
type: object
id:
type: string
# TODO: Add $ref here when documenting payment endpoints
govUkPayment:
required: false
type: object
LockedSessionResponse:
type: object
properties:
message:
type: string
enum:
- Session locked
paymentRequest:
type: object
properties:
id:
type: string
payeeEmail:
type: string
format: email
payeeName:
type: string
paths:
/resume-application:
post:
summary: Resume application
description: Request a "resume" email which lists all of your open applications. This email acts as a "dashboard" for the user.
tags:
- save and return
requestBody:
$ref: "#/components/schema/ResumeApplication"
responses:
"200":
$ref: "#/components/responses/ResumeApplication"
/validate-session:
post:
summary: Validate session
description: Validates the session and reconciles the session's breadcrumbs to account for any differences between the current published flow and the last flow version a user traversed.
tags:
- save and return
requestBody:
$ref: "#/components/schema/ValidateSession"
responses:
"200":
content:
application/json:
schema:
oneOf:
- $ref: "#/components/responses/ValidationResponse"
- $ref: "#/components/responses/LockedSessionResponse"
26 changes: 26 additions & 0 deletions api.planx.uk/modules/saveAndReturn/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Router } from "express";

import {
resumeApplicationController,
resumeApplicationSchema,
validateSessionController,
validateSessionSchema,
} from "./controller";
import { sendEmailLimiter } from "../../rateLimit";
import { validate } from "../../shared/middleware/validate";

const router = Router();

router.post(
"/resume-application",
sendEmailLimiter,
validate(resumeApplicationSchema),
resumeApplicationController,
);
router.post(
"/validate-session",
validate(validateSessionSchema),
validateSessionController,
);

export default router;
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { LowCalSession, Team } from "./../types";
import { LowCalSession, Team } from "../../../types";
import supertest from "supertest";
import app from "../server";
import { queryMock } from "../tests/graphqlQueryMock";
import { mockLowcalSession, mockTeam } from "../tests/mocks/saveAndReturnMocks";
import app from "../../../server";
import { queryMock } from "../../../tests/graphqlQueryMock";
import {
mockLowcalSession,
mockTeam,
} from "../../../tests/mocks/saveAndReturnMocks";
import { buildContentFromSessions } from "./resumeApplication";
import { PartialDeep } from "type-fest";

Expand Down Expand Up @@ -216,10 +219,8 @@ describe("Resume Application endpoint", () => {
.send(invalidBody)
.expect(400)
.then((response) => {
expect(response.body).toHaveProperty(
"error",
"Required value missing",
);
expect(response.body).toHaveProperty("issues");
expect(response.body).toHaveProperty("name", "ZodError");
});
}
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,44 +1,25 @@
import { NextFunction, Request, Response } from "express";
import { gql } from "graphql-request";
import { LowCalSession, Team } from "../types";
import { LowCalSession, Team } from "../../../types";
import { convertSlugToName, getResumeLink, calculateExpiryDate } from "./utils";
import { sendEmail } from "../notify";
import { sendEmail } from "../../../notify";
import type { SiteAddress } from "@opensystemslab/planx-core/types";
import { $api, $public } from "../client";
import { $api, $public } from "../../../client";

/**
* Send a "Resume" email to an applicant which list all open applications for a given council (team)
*/
const resumeApplication = async (
req: Request,
res: Response,
next: NextFunction,
) => {
try {
const { teamSlug, email } = req.body.payload;
if (!teamSlug || !email)
return next({
status: 400,
message: "Required value missing",
});
const resumeApplication = async (teamSlug: string, email: string) => {
const { team, sessions } = await validateRequest(teamSlug, email);
// Protect against phishing by returning a positive response even if no matching sessions found
if (!sessions.length) return { message: "Success" };

const { team, sessions } = await validateRequest(teamSlug, email);
// Protect against phishing by returning a positive response even if no matching sessions found
if (!sessions.length) return res.json({ message: "Success" });

const config = {
personalisation: await getPersonalisation(sessions, team),
reference: null,
emailReplyToId: team.notifyPersonalisation.emailReplyToId,
};
const response = await sendEmail("resume", email, config);
return res.json(response);
} catch (error) {
return next({
error,
message: `Failed to send "Resume" email. ${(error as Error).message}`,
});
}
const config = {
personalisation: await getPersonalisation(sessions, team),
reference: null,
emailReplyToId: team.notifyPersonalisation.emailReplyToId,
};
const response = await sendEmail("resume", email, config);
return response;
};

interface ValidateRequest {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Team } from "../types";
import { Team } from "../../../types";
import { convertSlugToName, getResumeLink } from "./utils";

describe("convertSlugToName util function", () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
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 { $api, $public } from "../client";
import { LowCalSession, Team } from "../../../types";
import { Template, getClientForTemplate, sendEmail } from "../../../notify";
import { $api, $public } from "../../../client";

const DAYS_UNTIL_EXPIRY = 28;
const REMINDER_DAYS_FROM_EXPIRY = [7, 1];
Expand Down
Loading

0 comments on commit 43e222f

Please sign in to comment.