From 51fde55b6d93ef1809f42cc78a38a5f9e1b95bb2 Mon Sep 17 00:00:00 2001 From: crystaltine Date: Tue, 25 Aug 2026 20:43:36 -0700 Subject: [PATCH 1/6] add /bulk/decide-applications registration endpoint --- .../registration/src/routes/application.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/services/registration/src/routes/application.ts b/services/registration/src/routes/application.ts index 5318b8f..05fab8e 100644 --- a/services/registration/src/routes/application.ts +++ b/services/registration/src/routes/application.ts @@ -825,3 +825,41 @@ applicationRouter.route("/:id/check-in-status").get( }); }) ); + +/** + * decide multiple applications, given a list of Ids. + * Returns how many applications were updated. + * + * This only allows updating to ACCEPTED, WAITLISTED, or DENIED. + */ +applicationRouter.route("/bulk/decide-applications").post( + checkAbility("manage", "Application"), + asyncHandler(async (req, res) => { + const { ids, newStatus } = req.body as { ids?: string[]; newStatus?: string }; + + if (!Array.isArray(ids) || typeof newStatus !== "string") { + throw new BadRequestError("ids: string[] and newStatus: StatusType (string) are required."); + } + if (ids.some(id => !isValidObjectId(id))) { + throw new BadRequestError("ids must contain only valid application IDs."); + } + + const validIds = ids.map(id => new Types.ObjectId(id)); + + // only allow this route to accept,wl,or deny + const ALLOWED_NEWSTATUSES = [StatusType.ACCEPTED, StatusType.WAITLISTED, StatusType.DENIED]; + if (!ALLOWED_NEWSTATUSES.includes(newStatus as StatusType)) { + throw new BadRequestError( + `This endpoint can only be used to set application statuses to ${ALLOWED_NEWSTATUSES.join(", ")}.` + ); + } + + const result = await ApplicationModel.accessibleBy(req.ability).updateMany( + { _id: { $in: validIds } }, + { $set: { status: newStatus } }, + { runValidators: true } + ); + + return res.status(200).json({ updatedCount: result.modifiedCount }); + }) +); From 583764750607046585b770d7e2fecf1c8674e126 Mon Sep 17 00:00:00 2001 From: crystaltine Date: Wed, 26 Aug 2026 12:32:31 -0700 Subject: [PATCH 2/6] add /bulk/emails-to-applications endpoint --- .../registration/src/routes/application.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/services/registration/src/routes/application.ts b/services/registration/src/routes/application.ts index 05fab8e..dba2279 100644 --- a/services/registration/src/routes/application.ts +++ b/services/registration/src/routes/application.ts @@ -863,3 +863,29 @@ applicationRouter.route("/bulk/decide-applications").post( return res.status(200).json({ updatedCount: result.modifiedCount }); }) ); + +/** + * bulk-fetch a list of application IDs given a list of emails. + * just ignores emails which have no matching application for + * the current hexathon and branch. + * + * currently used for the manual include/exclude list on the + * registration decisions page for applications (bulk status updates) + */ +applicationRouter.route("/bulk/emails-to-applications").post( + checkAbility("manage", "Application"), + asyncHandler(async (req, res) => { + const emails = req.body?.emails; + + if (!Array.isArray(emails)) { + throw new BadRequestError("List of emails required"); + } + + const result = await ApplicationModel.accessibleBy(req.ability).find( + { email: { $in: emails } }, + { projection: { _id: 1, name: 1, finalScore: 1, applicationBranch: 1 } } + ); + + return res.status(200).json(result); + }) +); From 4eaa20ba8117dbf986bcebeaae3ddaeb33e46b0e Mon Sep 17 00:00:00 2001 From: crystaltine Date: Wed, 26 Aug 2026 12:45:24 -0700 Subject: [PATCH 3/6] allow /bulk/decide-applications to autoset confirmation branch --- .../registration/src/routes/application.ts | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/services/registration/src/routes/application.ts b/services/registration/src/routes/application.ts index dba2279..8605943 100644 --- a/services/registration/src/routes/application.ts +++ b/services/registration/src/routes/application.ts @@ -835,7 +835,11 @@ applicationRouter.route("/:id/check-in-status").get( applicationRouter.route("/bulk/decide-applications").post( checkAbility("manage", "Application"), asyncHandler(async (req, res) => { - const { ids, newStatus } = req.body as { ids?: string[]; newStatus?: string }; + const { + ids, + newStatus, + confirmationBranchId, // optional, for acceptance only. ignored for not accepting. + } = req.body ?? {}; if (!Array.isArray(ids) || typeof newStatus !== "string") { throw new BadRequestError("ids: string[] and newStatus: StatusType (string) are required."); @@ -843,6 +847,12 @@ applicationRouter.route("/bulk/decide-applications").post( if (ids.some(id => !isValidObjectId(id))) { throw new BadRequestError("ids must contain only valid application IDs."); } + // require and only allow confirmation branch if ACCEPTING + if (newStatus === StatusType.ACCEPTED && !confirmationBranchId) { + throw new BadRequestError( + "confirmationBranchId is required when updating status to ACCEPTED." + ); + } const validIds = ids.map(id => new Types.ObjectId(id)); @@ -854,9 +864,15 @@ applicationRouter.route("/bulk/decide-applications").post( ); } + const updater = { + $set: { + status: newStatus, + confirmationBranch: newStatus === StatusType.ACCEPTED ? confirmationBranchId : undefined, + }, + }; const result = await ApplicationModel.accessibleBy(req.ability).updateMany( { _id: { $in: validIds } }, - { $set: { status: newStatus } }, + updater, { runValidators: true } ); From ac996d4b7833dabf9ea88cf84e27b1b741b1cdb9 Mon Sep 17 00:00:00 2001 From: crystaltine Date: Sat, 29 Aug 2026 13:21:22 -0400 Subject: [PATCH 4/6] make bulk/decide-applications only update APPLIED applications --- services/registration/src/routes/application.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/registration/src/routes/application.ts b/services/registration/src/routes/application.ts index 8605943..aebf1ce 100644 --- a/services/registration/src/routes/application.ts +++ b/services/registration/src/routes/application.ts @@ -871,7 +871,7 @@ applicationRouter.route("/bulk/decide-applications").post( }, }; const result = await ApplicationModel.accessibleBy(req.ability).updateMany( - { _id: { $in: validIds } }, + { _id: { $in: validIds }, status: StatusType.APPLIED }, updater, { runValidators: true } ); From 62fad869fff02536f56ac1233ac2ed07b0146839 Mon Sep 17 00:00:00 2001 From: crystaltine Date: Fri, 4 Sep 2026 19:07:04 -0400 Subject: [PATCH 5/6] fix projection syntax + add hexathon/branch filter for registration/bulk/emails-to-applications --- services/registration/src/routes/application.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/services/registration/src/routes/application.ts b/services/registration/src/routes/application.ts index aebf1ce..6a5c490 100644 --- a/services/registration/src/routes/application.ts +++ b/services/registration/src/routes/application.ts @@ -897,10 +897,16 @@ applicationRouter.route("/bulk/emails-to-applications").post( throw new BadRequestError("List of emails required"); } - const result = await ApplicationModel.accessibleBy(req.ability).find( - { email: { $in: emails } }, - { projection: { _id: 1, name: 1, finalScore: 1, applicationBranch: 1 } } - ); + // require hexathon and branch for this + const hexathonId = req.body?.hexathonId; + const branchId = req.body?.branchId; + if (!hexathonId || !branchId) { + throw new BadRequestError("hexathonId and branchId are required."); + } + + const result = await ApplicationModel.accessibleBy(req.ability) + .find({ email: { $in: emails }, hexathon: hexathonId, applicationBranch: branchId }) + .select({ _id: 1, name: 1, finalScore: 1, applicationBranch: 1 }); return res.status(200).json(result); }) From 9ab0dd34b6809443ecef21a069014dc8f922e8ec Mon Sep 17 00:00:00 2001 From: crystaltine Date: Fri, 4 Sep 2026 19:17:58 -0400 Subject: [PATCH 6/6] improve readability in registration bulk update endpoints --- services/registration/src/routes/application.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/services/registration/src/routes/application.ts b/services/registration/src/routes/application.ts index 6a5c490..e474bcb 100644 --- a/services/registration/src/routes/application.ts +++ b/services/registration/src/routes/application.ts @@ -841,6 +841,7 @@ applicationRouter.route("/bulk/decide-applications").post( confirmationBranchId, // optional, for acceptance only. ignored for not accepting. } = req.body ?? {}; + // VALIDATIONS ===== if (!Array.isArray(ids) || typeof newStatus !== "string") { throw new BadRequestError("ids: string[] and newStatus: StatusType (string) are required."); } @@ -853,9 +854,6 @@ applicationRouter.route("/bulk/decide-applications").post( "confirmationBranchId is required when updating status to ACCEPTED." ); } - - const validIds = ids.map(id => new Types.ObjectId(id)); - // only allow this route to accept,wl,or deny const ALLOWED_NEWSTATUSES = [StatusType.ACCEPTED, StatusType.WAITLISTED, StatusType.DENIED]; if (!ALLOWED_NEWSTATUSES.includes(newStatus as StatusType)) { @@ -863,18 +861,21 @@ applicationRouter.route("/bulk/decide-applications").post( `This endpoint can only be used to set application statuses to ${ALLOWED_NEWSTATUSES.join(", ")}.` ); } + // ===== + const filter = { + _id: { $in: ids.map(id => new Types.ObjectId(id)) }, + status: StatusType.APPLIED, + }; const updater = { $set: { status: newStatus, confirmationBranch: newStatus === StatusType.ACCEPTED ? confirmationBranchId : undefined, }, }; - const result = await ApplicationModel.accessibleBy(req.ability).updateMany( - { _id: { $in: validIds }, status: StatusType.APPLIED }, - updater, - { runValidators: true } - ); + const result = await ApplicationModel.accessibleBy(req.ability).updateMany(filter, updater, { + runValidators: true, + }); return res.status(200).json({ updatedCount: result.modifiedCount }); })