Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Production deploy #2433

Merged
merged 2 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading