Skip to content

Commit

Permalink
Final RSVP nits (#168)
Browse files Browse the repository at this point in the history
* Final RSVP nits

* Testing

* update rsvp logic, fix docs

* fix tests

* typo

* fix docs

---------

Co-authored-by: Lasya <neti.lasya@gmail.com>
  • Loading branch information
AydanPirani and lasyaneti authored Jan 22, 2024
1 parent 4465937 commit aeea6a6
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 107 deletions.
6 changes: 5 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ export enum Device {
}

export enum RegistrationTemplates {
REGISTRATION_SUBMISSION = "registration_confirmation",
REGISTRATION_SUBMISSION = "2024_registration_confirmation",
STATUS_UPDATE = "2024_status_update",
RSVP_CONFIRMATION = "2024_rsvp_confirmation",
RSVP_REMINDER_1_WEEK = "2024_rsvp-reminder-1week",
RSVP_REMINDER_1_DAY = "2024_rsvp-reminder",
}

export enum Avatars {
Expand Down
30 changes: 7 additions & 23 deletions src/database/admission-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,31 +20,15 @@ export class AdmissionDecision {
@prop({ required: true })
public status: DecisionStatus;

@prop({ required: false })
@prop({ default: false })
public admittedPro?: boolean;

@prop({ required: true })
public response: DecisionResponse;
@prop({ default: DecisionResponse.PENDING })
public response?: DecisionResponse;

@prop({ required: true })
public reviewer: string;
@prop({ default: false })
public emailSent?: boolean;

@prop({ required: true })
public emailSent: boolean;

constructor(
userId: string,
status: DecisionStatus,
response: DecisionResponse,
reviewer: string,
emailSent: boolean,
admittedPro?: boolean,
) {
this.userId = userId;
this.status = status;
this.admittedPro = admittedPro;
this.response = response;
this.reviewer = reviewer;
this.emailSent = emailSent;
}
@prop({ default: 0 })
public reimbursementValue?: number;
}
19 changes: 19 additions & 0 deletions src/services/admission/admission-formats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { isArrayOfType, isBoolean, isEnumOfType, isNumber, isString } from "../../formatTools.js";
import { AdmissionDecision, DecisionResponse, DecisionStatus } from "../../database/admission-db.js";

export function isValidApplicantFormat(obj: AdmissionDecision[]): boolean {
return isArrayOfType(obj, isValidApplicantDecision);
}

function isValidApplicantDecision(obj: unknown): boolean {
const decision = obj as AdmissionDecision;

return (
isString(decision.userId) &&
isEnumOfType(decision.status, DecisionStatus) &&
(decision.response === undefined || isEnumOfType(decision.response, DecisionResponse)) &&
isBoolean(decision.emailSent) &&
(decision.admittedPro === undefined || isBoolean(decision.admittedPro)) &&
(decision.reimbursementValue === undefined || (isNumber(decision.reimbursementValue) && decision.reimbursementValue >= 0))
);
}
21 changes: 16 additions & 5 deletions src/services/admission/admission-router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,26 @@ const TESTER_DECISION = {
status: DecisionStatus.ACCEPTED,
response: DecisionResponse.PENDING,
emailSent: false,
reviewer: "tester-reviewer",
} satisfies AdmissionDecision;

const OTHER_DECISION = {
userId: "other-user",
status: DecisionStatus.REJECTED,
response: DecisionResponse.DECLINED,
emailSent: true,
reviewer: "other-reviewer",
} satisfies AdmissionDecision;

const updateRequest = [
{
userId: TESTER.id,
status: DecisionStatus.WAITLISTED,
response: DecisionResponse.PENDING,
reviewer: "",
emailSent: false,
},
{
userId: "other-user",
status: DecisionStatus.ACCEPTED,
response: DecisionResponse.PENDING,
reviewer: "",
emailSent: false,
},
] satisfies AdmissionDecision[];
Expand All @@ -58,9 +54,11 @@ describe("PUT /admission/update/", () => {
const responseUser = await putAsUser("/admission/update/").send(updateRequest).expect(StatusCode.ClientErrorForbidden);
expect(JSON.parse(responseUser.text)).toHaveProperty("error", "Forbidden");
});

it("should update application status of applicants", async () => {
const response = await putAsStaff("/admission/update/").send(updateRequest).expect(StatusCode.SuccessOK);
expect(JSON.parse(response.text)).toHaveProperty("message", "StatusSuccess");

const ops = updateRequest.map((entry) => Models.AdmissionDecision.findOne({ userId: entry.userId }));
const retrievedEntries = await Promise.all(ops);
expect(retrievedEntries).toMatchObject(
Expand Down Expand Up @@ -95,7 +93,20 @@ describe("GET /admission/rsvp/", () => {
it("works for a staff user and returns unfiltered data", async () => {
const response = await getAsStaff("/admission/rsvp/").expect(StatusCode.SuccessOK);

expect(JSON.parse(response.text)).toMatchObject(TESTER_DECISION);
// expect(JSON.parse(response.text)).toMatchObject(TESTER_DECISION);
expect(JSON.parse(response.text)).toEqual(
expect.arrayContaining([
expect.objectContaining({
// Specify the properties of AdmissionDecision since its an array of custom model
userId: expect.any(String),
status: expect.any(String),
response: expect.any(String),
admittedPro: expect.any(Boolean),
emailSent: expect.any(Boolean),
reimbursementValue: expect.any(Number),
}),
]),
);
});
});

Expand Down
127 changes: 62 additions & 65 deletions src/services/admission/admission-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { JwtPayload } from "../auth/auth-models.js";
import { DecisionStatus, DecisionResponse, AdmissionDecision } from "../../database/admission-db.js";
import Models from "../../database/models.js";
import { hasElevatedPerms } from "../auth/auth-lib.js";
import { isValidApplicantFormat } from "./admission-formats.js";
import { StatusCode } from "status-code-enum";
import { NextFunction } from "express-serve-static-core";
import { RouterError } from "../../middleware/error-handler.js";
Expand All @@ -25,22 +26,22 @@ const admissionRouter: Router = Router();
* "userId": "user1",
* "status": "ACCEPTED",
* "response": "ACCEPTED",
* "reviewer": "reviewer1",
* "emailSent": false
* "emailSent": false,
* "reimbursementValue": 100,
* },
* {
* "userId": "user3",
* "status": "WAITLISTED",
* "response": "PENDING",
* "reviewer": "reviewer1",
* "emailSent": false
* "emailSent": false,
* "reimbursementValue": 100,
* },
* {
* "userId": "user4",
* "status": "WAITLISTED",
* "response": "PENDING",
* "reviewer": "reviewer1",
* "emailSent": false
* "emailSent": false,
* "reimbursementValue": 100,
* }
* ]
* @apiUse strongVerifyErrors
Expand All @@ -62,19 +63,19 @@ admissionRouter.get("/notsent/", strongJwtVerification, async (_: Request, res:
* @apiGroup Admission
* @apiDescription Updates an rsvp for the currently authenticated user (determined by the JWT in the Authorization header).
*
* @apiSuccess (200: Success) {string} userId
* @apiSuccess (200: Success) {string} User's application status
* @apiSuccess (200: Success) {string} User's Response (whether or whether not they're attending)
* @apiSuccess (200: Success) {string} Reviwer
* @apiSuccess (200: Success) {boolean} Whether email has been sent
* @apiSuccess (200: Success) {string} userId userId
* @apiSuccess (200: Success) {string} status User's application status
* @apiSuccess (200: Success) {string} response User's Response (whether or whether not they're attending)
* @apiSuccess (200: Success) {boolean} emailSent Whether email has been sent
* @apiSuccess (200: Success) {boolean} reimbursementValue Amount reimbursed
* @apiSuccessExample Example Success Response:
* HTTP/1.1 200 OK
* {
* "userId": "github0000001",
* "status": "ACCEPTED",
* "response": "DECLINED",
* "reviewer": "reviewer1",
* "emailSent": true
* "emailSent": true,
* "reimbursementValue": 100
* }
*
* @apiUse strongVerifyErrors
Expand Down Expand Up @@ -114,19 +115,19 @@ admissionRouter.put("/rsvp/accept/", strongJwtVerification, async (_: Request, r
* @apiGroup Admission
* @apiDescription Updates an rsvp for the currently authenticated user (determined by the JWT in the Authorization header).
*
* @apiSuccess (200: Success) {string} userId
* @apiSuccess (200: Success) {string} User's application status
* @apiSuccess (200: Success) {string} User's Response (whether or whether not they're attending)
* @apiSuccess (200: Success) {string} Reviwer
* @apiSuccess (200: Success) {boolean} Whether email has been sent
* @apiSuccess (200: Success) {string} userId userId
* @apiSuccess (200: Success) {string} status User's application status
* @apiSuccess (200: Success) {string} response User's Response (whether or whether not they're attending)
* @apiSuccess (200: Success) {boolean} emailSent Whether email has been sent
* @apiSuccess (200: Success) {boolean} reimbursementValue Amount reimbursed
* @apiSuccessExample Example Success Response:
* HTTP/1.1 200 OK
* {
* "userId": "github0000001",
* "status": "ACCEPTED",
* "response": "DECLINED",
* "reviewer": "reviewer1",
* "emailSent": true
* "emailSent": true,
* "reimbursementValue": 100
* }
*
* @apiUse strongVerifyErrors
Expand Down Expand Up @@ -173,21 +174,22 @@ admissionRouter.put("/rsvp/decline/", strongJwtVerification, async (_: Request,
* @apiParamExample Example Request (Staff):
* HTTP/1.1 PUT /admission/
* [
* {
* "userId": "user1",
* "status": "ACCEPTED",
* "admittedPro": false,
* },
* {
* "userId": "user2",
* "status": "REJECTED",
* "admittedPro": true,
* },
* {
* "userId": "user3",
* "status": "WAITLISTED",
* "admittedPro": true,
* }
* {
* "userId": "github44285522",
* "admittedPro": false,
* "reimbursementValue": 100,
* "status": "ACCEPTED",
* "emailSent": true,
* "response": "PENDING"
* },
* {
* "userId": "github4fsfs22",
* "admittedPro": false,
* "reimbursementValue": 50,
* "status": "ACCEPTED",
* "emailSent": true,
* "response": "PENDING"
* }
* ]
*
* @apiSuccess (200: Success) {String} StatusSuccess
Expand All @@ -204,10 +206,15 @@ admissionRouter.put("/update/", strongJwtVerification, async (req: Request, res:
}

const updateEntries: AdmissionDecision[] = req.body as AdmissionDecision[];

if (!isValidApplicantFormat(updateEntries)) {
return next(new RouterError(StatusCode.ClientErrorBadRequest, "BadRequest"));
}

const ops = updateEntries.map((entry) =>
Models.AdmissionDecision.findOneAndUpdate(
{ userId: entry.userId },
{ $set: { status: entry.status, admittedPro: entry.admittedPro } },
{ $set: { status: entry.status, admittedPro: entry.admittedPro, reimbursementValue: entry.reimbursementValue } },
),
);

Expand All @@ -222,30 +229,21 @@ admissionRouter.put("/update/", strongJwtVerification, async (req: Request, res:
/**
* @api {get} /admission/rsvp/ GET /admission/rsvp/
* @apiGroup Admission
* @apiDescription Check RSVP decision for current user, returns filtered info for attendees and unfiltered info for staff/admin.
* @apiDescription Get RSVP decision for current user. Returns applicant's info if non-staff. Returns all RSVP data if staff.
*
* @apiSuccess (200: Success) {string} userId
* @apiSuccess (200: Success) {string} status User's applicatoin status
* @apiSuccess (200: Success) {string} response User's Response (whether or whether not they're attending)
* @apiSuccess (200: Success) {string} admittedPro Indicates whether applicant was admitted into pro or not. Reference registration data to determine if their acceptance was deferred or direct.
* @apiSuccessExample Example Success Response (caller is a user):
* HTTP/1.1 200 OK
* {
* "userId": "github0000001",
* "status": "ACCEPTED",
* "response": "ACCEPTED",
* "admittedPro": false,
* }
*
* @apiSuccessExample Example Success Response (caller is a staff/admin):
* @apiSuccessExample Example Success Response:
* HTTP/1.1 200 OK
* {
* "userId": "github0000001",
* "status": "ACCEPTED",
* "response": "ACCEPTED",
* "reviewer": "reviewer1",
* "emailSent": true,
* "admittedPro": true
* "admittedPro": false,
* "reimbursementValue": 100
* }
*
* @apiUse strongVerifyErrors
Expand All @@ -254,23 +252,22 @@ admissionRouter.get("/rsvp/", strongJwtVerification, async (_: Request, res: Res
const payload: JwtPayload = res.locals.payload as JwtPayload;
const userId: string = payload.id;

if (hasElevatedPerms(payload)) {
const staffQueryResult: AdmissionDecision[] | null = await Models.AdmissionDecision.find();
//Returns error if query is empty
if (!staffQueryResult) {
return next(new RouterError(StatusCode.ClientErrorNotFound, "UserNotFound"));
}
return res.status(StatusCode.SuccessOK).send(staffQueryResult);
}

const queryResult: AdmissionDecision | null = await Models.AdmissionDecision.findOne({ userId: userId });

//Returns error if query is empty
if (!queryResult) {
return next(new RouterError(StatusCode.ClientErrorNotFound, "UserNotFound"));
}

//Filters data if caller doesn't have elevated perms
if (!hasElevatedPerms(payload)) {
return res.status(StatusCode.SuccessOK).send({
userId: queryResult.userId,
status: queryResult.status,
response: queryResult.response,
admittedPro: queryResult.admittedPro,
});
}

return res.status(StatusCode.SuccessOK).send(queryResult);
});

Expand All @@ -282,18 +279,18 @@ admissionRouter.get("/rsvp/", strongJwtVerification, async (_: Request, res: Res
* @apiParam {String} USERID Id to pull the decision for
*
* @apiSuccess (200: Success) {string} userId
* @apiSuccess (200: Success) {string} User's application status
* @apiSuccess (200: Success) {string} User's Response (whether or whether not they're attending)
* @apiSuccess (200: Success) {string} Reviewer
* @apiSuccess (200: Success) {boolean} Whether email has been sent
* @apiSuccess (200: Success) {string} status User's application status
* @apiSuccess (200: Success) {string} response User's Response (whether or whether not they're attending)
* @apiSuccess (200: Success) {boolean} emailSent Whether email has been sent
* @apiSuccess (200: Success) {int} reimbursementValue Amount reimbursed
* @apiSuccessExample Example Success Response:
* HTTP/1.1 200 OK
* {
* "userId": "github0000001",
* "status": "ACCEPTED",
* "response": "PENDING",
* "reviewer": "reviewer1",
* "emailSent": true
* "emailSent": true,
* "reimbursementValue": 0
* }
*
* @apiUse strongVerifyErrors
Expand Down
1 change: 1 addition & 0 deletions src/services/mail/mail-formats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface MailInfoFormat {
templateId: string;
recipients: string[];
scheduleTime?: string;
subs?: object;
}

export function isValidMailInfo(mailInfo: MailInfoFormat): boolean {
Expand Down
Loading

0 comments on commit aeea6a6

Please sign in to comment.