From fadaefafde71ebb015e184557b4d23811ff1467e Mon Sep 17 00:00:00 2001 From: Tyffoni Date: Mon, 5 Jan 2026 22:03:35 -0800 Subject: [PATCH 1/8] added membership payment schema --- .../models/MembershipPayment.js | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 api/main_endpoints/models/MembershipPayment.js diff --git a/api/main_endpoints/models/MembershipPayment.js b/api/main_endpoints/models/MembershipPayment.js new file mode 100644 index 000000000..6b3e24712 --- /dev/null +++ b/api/main_endpoints/models/MembershipPayment.js @@ -0,0 +1,40 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + + +const MembershipPaymentSchema = new Schema( + { + createdAt: { + type: Date, + default: Date.now + }, + userId: { + type: Schema.Types.ObjectId, + ref: 'User', + default: null, + }, + status: { + type: String, + enum: Object.keys({PENDING: 'PENDING', COMPLETED: 'COMPLETED', FAILED: 'FAILED' }), + default: 'PENDING', + required: true, + }, + confirmationCode: { + type: String, + required: true, + }, + amount: { + type: Number, + required: true, + }, + venmoDetails:{ + transactionId: { type: String, default: '' }, + payerName: { type: String, default: '' }, + note: { type: String, default: '' }, + } + }, + { collection: 'MembershipPayments' } +); + + +module.exports = mongoose.model('MembershipPayment.js', MembershipPaymentSchema); From 469da467eab7ab3cd9261f710395922135375042 Mon Sep 17 00:00:00 2001 From: Tyffoni Date: Mon, 5 Jan 2026 22:05:47 -0800 Subject: [PATCH 2/8] revised spacing --- api/main_endpoints/models/MembershipPayment.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/api/main_endpoints/models/MembershipPayment.js b/api/main_endpoints/models/MembershipPayment.js index 6b3e24712..22999a55a 100644 --- a/api/main_endpoints/models/MembershipPayment.js +++ b/api/main_endpoints/models/MembershipPayment.js @@ -24,13 +24,13 @@ const MembershipPaymentSchema = new Schema( required: true, }, amount: { - type: Number, - required: true, + type: Number, + required: true, }, venmoDetails:{ - transactionId: { type: String, default: '' }, - payerName: { type: String, default: '' }, - note: { type: String, default: '' }, + transactionId: { type: String, default: '' }, + payerName: { type: String, default: '' }, + note: { type: String, default: '' }, } }, { collection: 'MembershipPayments' } From 79276c8949fbba4825d9655437c25898bcf7172f Mon Sep 17 00:00:00 2001 From: Tiffany Tran <167036717+Tyffoni@users.noreply.github.com> Date: Mon, 5 Jan 2026 22:08:56 -0800 Subject: [PATCH 3/8] Update api/main_endpoints/models/MembershipPayment.js Co-authored-by: adarsh <110150037+adarshm11@users.noreply.github.com> --- api/main_endpoints/models/MembershipPayment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/main_endpoints/models/MembershipPayment.js b/api/main_endpoints/models/MembershipPayment.js index 22999a55a..b45d42dc1 100644 --- a/api/main_endpoints/models/MembershipPayment.js +++ b/api/main_endpoints/models/MembershipPayment.js @@ -15,7 +15,7 @@ const MembershipPaymentSchema = new Schema( }, status: { type: String, - enum: Object.keys({PENDING: 'PENDING', COMPLETED: 'COMPLETED', FAILED: 'FAILED' }), + enum: ['pending', 'completed', 'rejected']), default: 'PENDING', required: true, }, From 1eec6804ca00ec8ab272bfd3e4ef00e19d9b0da8 Mon Sep 17 00:00:00 2001 From: Tyffoni Date: Mon, 5 Jan 2026 22:10:58 -0800 Subject: [PATCH 4/8] fixed syntax --- api/main_endpoints/models/MembershipPayment.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api/main_endpoints/models/MembershipPayment.js b/api/main_endpoints/models/MembershipPayment.js index b45d42dc1..7180eec29 100644 --- a/api/main_endpoints/models/MembershipPayment.js +++ b/api/main_endpoints/models/MembershipPayment.js @@ -15,12 +15,13 @@ const MembershipPaymentSchema = new Schema( }, status: { type: String, - enum: ['pending', 'completed', 'rejected']), - default: 'PENDING', + enum: ['pending', 'completed', 'rejected'], + default: 'pending', required: true, }, confirmationCode: { type: String, + unique: true, required: true, }, amount: { From 197dd1369cd697971d8eb2640c037e4961274bae Mon Sep 17 00:00:00 2001 From: adarsh <110150037+adarshm11@users.noreply.github.com> Date: Mon, 5 Jan 2026 22:12:09 -0800 Subject: [PATCH 5/8] Update api/main_endpoints/models/MembershipPayment.js --- api/main_endpoints/models/MembershipPayment.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/main_endpoints/models/MembershipPayment.js b/api/main_endpoints/models/MembershipPayment.js index 7180eec29..e0c0f52df 100644 --- a/api/main_endpoints/models/MembershipPayment.js +++ b/api/main_endpoints/models/MembershipPayment.js @@ -29,9 +29,9 @@ const MembershipPaymentSchema = new Schema( required: true, }, venmoDetails:{ - transactionId: { type: String, default: '' }, - payerName: { type: String, default: '' }, - note: { type: String, default: '' }, + transactionId: { type: String }, + payerName: { type: String }, + note: { type: String }, } }, { collection: 'MembershipPayments' } From 56ca2bb5fe7e57b935181532643bd788a3a3a88c Mon Sep 17 00:00:00 2001 From: adarsh <110150037+adarshm11@users.noreply.github.com> Date: Mon, 5 Jan 2026 22:13:35 -0800 Subject: [PATCH 6/8] Update api/main_endpoints/models/MembershipPayment.js --- api/main_endpoints/models/MembershipPayment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/main_endpoints/models/MembershipPayment.js b/api/main_endpoints/models/MembershipPayment.js index e0c0f52df..cdfe8a5bd 100644 --- a/api/main_endpoints/models/MembershipPayment.js +++ b/api/main_endpoints/models/MembershipPayment.js @@ -38,4 +38,4 @@ const MembershipPaymentSchema = new Schema( ); -module.exports = mongoose.model('MembershipPayment.js', MembershipPaymentSchema); +module.exports = mongoose.model('MembershipPayment', MembershipPaymentSchema); From 21c0284026b4d755b856e69802f74504ceebbee7 Mon Sep 17 00:00:00 2001 From: Emily <162777193+codebyemily@users.noreply.github.com> Date: Tue, 6 Jan 2026 18:37:21 -0800 Subject: [PATCH 7/8] Emily/20250104 handle membership payment confirmation (#1993) * initial commit * fixed spacing * fixed spacing 2 * fixed spacing 2 * updated logic and merged find and Verify functions * fix spacing * refactor: improve payment verification and rejection logic * fix * refactor: improve error handling in membership payment verification and rejection * fix * final fixes * final fixes * refactored updateMembershipExpiration * fix spacing --- .../routes/MembershipPayment.js | 71 +++++++++++++++++++ .../util/membershipPaymentQueries.js | 62 ++++++++++++++++ api/main_endpoints/util/userHelpers.js | 25 +++++++ 3 files changed, 158 insertions(+) create mode 100644 api/main_endpoints/routes/MembershipPayment.js create mode 100644 api/main_endpoints/util/membershipPaymentQueries.js diff --git a/api/main_endpoints/routes/MembershipPayment.js b/api/main_endpoints/routes/MembershipPayment.js new file mode 100644 index 000000000..093dd8817 --- /dev/null +++ b/api/main_endpoints/routes/MembershipPayment.js @@ -0,0 +1,71 @@ +const express = require('express'); +const router = express.Router(); +const bodyParser = require('body-parser'); +router.use(bodyParser.json()); +const { + BAD_REQUEST, + SERVER_ERROR, + NOT_FOUND, + OK, +} = require('../../util/constants').STATUS_CODES; +const membershipState = require('../../util/constants').MEMBERSHIP_STATE; +const User = require('../models/User'); +const { getMemberExpirationDate, updateMembershipExpiration } = require('../util/userHelpers'); +const { findVerifyPayment, rejectPayment } = require('../util/membershipPaymentQueries'); +const { decodeToken } = require('../util/token-functions.js'); + +router.post('/verifyMembership', async (req, res) => { + const decoded = await decodeToken(req, membershipState.PENDING); + if (decoded.status !== OK) { + return res.sendStatus(decoded.status); + } + + const { confirmationCode } = req.body; + const userId = decoded.token._id; + + if (!confirmationCode) { + return res.sendStatus(BAD_REQUEST); + } + + const paymentDocument = await findVerifyPayment(confirmationCode, userId); + if (paymentDocument === null){ + return res.sendStatus(SERVER_ERROR); + } + if (paymentDocument === false){ + return res.sendStatus(NOT_FOUND); + } + + const paymentId = paymentDocument._id; + const { amount } = paymentDocument; + + if (amount < 20){ + const rejected = await rejectPayment(paymentId); + if (rejected === null){ + return res.sendStatus(SERVER_ERROR); + } + if (rejected === false){ + return res.sendStatus(NOT_FOUND); + } + return res.sendStatus(BAD_REQUEST); + } + + let semestersToAdd = 0; + if (amount >= 30) { + semestersToAdd = 2; + } else { + semestersToAdd = 1; + } + + const membershipUpdateResult = await updateMembershipExpiration( + decoded.token._id, + semestersToAdd + ); + + if (membershipUpdateResult === null) { + return res.sendStatus(SERVER_ERROR); + } + if (membershipUpdateResult === false) { + return res.status(NOT_FOUND).send('User not found.'); + } + return res.sendStatus(OK); +}); diff --git a/api/main_endpoints/util/membershipPaymentQueries.js b/api/main_endpoints/util/membershipPaymentQueries.js new file mode 100644 index 000000000..27361c2ba --- /dev/null +++ b/api/main_endpoints/util/membershipPaymentQueries.js @@ -0,0 +1,62 @@ +import MembershipPayment from '../main_endpoints/models/MembershipPayment.js'; + +const status = { + PENDING: 'pending', + COMPLETED: 'completed', + REJECTED: 'rejected', +}; + +function findVerifyPayment(confirmationCode, userId) { + return new Promise((resolve) => { + try { + MembershipPayment.findOneAndUpdate( + { + confirmationCode, + status: status.PENDING, + }, + { + $set: { userId, status: status.COMPLETED }, + }, + { + new: true, + runValidators: true, + }, + (error, result) => { + if (error) { + return resolve(null); + } + if (!result) { + return resolve(false); + } + return resolve(result); + } + ); + } catch (error) { + return resolve(null); + } + }); +} + +function rejectPayment(paymentId) { + return new Promise((resolve) => { + try { + MembershipPayment.findByIdAndUpdate( + paymentId, + { $set: { status: status.REJECTED } }, + (error, result) => { + if (error) { + return resolve(null); + } + if (!result) { + return resolve(false); + } + return resolve(true); + } + ); + } catch (error) { + return resolve(null); + } + }); +} + +module.exports = { findVerifyPayment, rejectPayment }; diff --git a/api/main_endpoints/util/userHelpers.js b/api/main_endpoints/util/userHelpers.js index 8ac327872..022aa9abc 100644 --- a/api/main_endpoints/util/userHelpers.js +++ b/api/main_endpoints/util/userHelpers.js @@ -203,6 +203,30 @@ function checkIfPageCountResets(lastLogin) { return lastLoginWasOverOneWeekAgo || aSundayHasPassedSinceLastLogin; } +/** + * Update a user's membershipValidUntil date + * @param {String} userId - The user's ID + * @param {Number} numberOfSemestersToSignUpFor - Number of semesters to extend + * @returns {Object} result - Contains success status and message + */ +async function updateMembershipExpiration(userId, numberOfSemestersToSignUpFor) { + try { + const newExpiration = getMemberExpirationDate(numberOfSemestersToSignUpFor); + const user = await User.findByIdAndUpdate( + userId, + { membershipValidUntil: newExpiration }, + { new: true }, + ); + if (!user) { + return false; + } + return true; + } catch (error) { + logger.error('Error updating membership:', error); + return null; + } +} + module.exports = { registerUser, getMemberExpirationDate, @@ -211,4 +235,5 @@ module.exports = { userWithEmailExists, checkIfPageCountResets, findPasswordReset, + updateMembershipExpiration }; From c6805be88f64c91ffbb12aab2b0d5f82876fae8a Mon Sep 17 00:00:00 2001 From: Charlynn Nguyen <129462659+charred-70@users.noreply.github.com> Date: Mon, 12 Jan 2026 14:48:52 -0800 Subject: [PATCH 8/8] created route to email users their confirmation code (#1997) * i pray i fixed my origin * added instructions * fixed import error * fixed template formatting * made function to call email route --------- Co-authored-by: Charlynn Nguyen --- .../membershipConfirmationCode.js | 20 +++++++ api/cloud_api/routes/Mailer.js | 52 ++++++++++++++++++- api/main_endpoints/util/emailHelpers.js | 14 ++++- 3 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 api/cloud_api/email_templates/membershipConfirmationCode.js diff --git a/api/cloud_api/email_templates/membershipConfirmationCode.js b/api/cloud_api/email_templates/membershipConfirmationCode.js new file mode 100644 index 000000000..ff02e9d0f --- /dev/null +++ b/api/cloud_api/email_templates/membershipConfirmationCode.js @@ -0,0 +1,20 @@ +function membershipConfirmationCode(user, recipient, confirmCode) { + return new Promise((resolve, reject) => { + return resolve({ + from: user, + to: recipient, + subject: 'SCE Membership Confirmation', + generateTextFromHTML: true, + html: ` +

+ Hi,
+ Thank you for signing up for membership!
+ Please use the below confirmation code when you + visit your profile page on the + SCE website to verify your membership:
${confirmCode} +

+ ` + }); + }); +} +module.exports = { membershipConfirmationCode }; diff --git a/api/cloud_api/routes/Mailer.js b/api/cloud_api/routes/Mailer.js index dca18e3be..605359f29 100644 --- a/api/cloud_api/routes/Mailer.js +++ b/api/cloud_api/routes/Mailer.js @@ -5,9 +5,11 @@ const { verification } = require('../email_templates/verification'); const { passwordReset } = require('../email_templates/passwordReset'); const { blastEmail } = require('../email_templates/blastEmail'); const { unsubscribeEmail } = require('../email_templates/unsubscribeEmail'); +const { membershipConfirmationCode } = require('../email_templates/membershipConfirmationCode'); const { OK, - BAD_REQUEST + BAD_REQUEST, + SERVER_ERROR } = require('../../util/constants').STATUS_CODES; const logger = require('../../util/logger'); const { googleApiKeys } = require('../../config/config.json'); @@ -134,4 +136,52 @@ router.post('/sendUnsubscribeEmail', async (req, res) => { return res.sendStatus(OK); }); +router.post('/sendMembershipConfirmationCode', async (req, res) => { + if (!ENABLED && process.env.NODE_ENV !== 'test') { + return res.sendStatus(OK); + } + const scopes = ['https://mail.google.com/']; + const pathToToken = __dirname + '/../../config/token.json'; + const apiHandler = new SceGoogleApiHandler(scopes, pathToToken); + const tokenJson = await apiHandler.checkIfTokenFileExists(); + + if (tokenJson) { + if (apiHandler.checkIfTokenIsExpired(tokenJson)) { + logger.warn('refreshing token'); + MetricsHandler.gcpRefreshTokenLastUpdated.set(Math.floor(Date.now() / 1000)); + apiHandler.refreshToken(); + } + } else { + logger.warn('getting new token! ', { tokenJson }); + apiHandler.getNewToken(); + } + + const { recipientEmail, confirmationCode } = req.body; + + if (!recipientEmail || !confirmationCode) { + logger.warn('Missing recipientEmail or confirmationCode', { body: req.body }); + return res.status(BAD_REQUEST).json({ + error: 'recipientEmail and confirmationCode are required', + }); + } + + await membershipConfirmationCode(USER, recipientEmail, confirmationCode) + .then((template) => { + apiHandler + .sendEmail(template) + .then((_) => { + res.sendStatus(OK); + MetricsHandler.emailSent.inc({ type: 'membershipConfirmationCode' }); + }) + .catch((err) => { + logger.error('unable to send confirmation code: ', err); + res.sendStatus(SERVER_ERROR); + }); + }) + .catch((err) => { + logger.error('unable to generate member confirmation email template: ', err); + res.sendStatus(SERVER_ERROR); + }); +}); + module.exports = router; diff --git a/api/main_endpoints/util/emailHelpers.js b/api/main_endpoints/util/emailHelpers.js index f9251f951..9742ff80a 100644 --- a/api/main_endpoints/util/emailHelpers.js +++ b/api/main_endpoints/util/emailHelpers.js @@ -40,4 +40,16 @@ async function sendPasswordReset(resetToken, email) { }); } -module.exports = { sendUnsubscribeEmail, sendVerificationEmail, sendPasswordReset }; +async function membershipConfirmationCode(confirmCode, email) { + return new Promise((resolve) => { + axios + .post(`${MAILER_API_URL}/Mailer/sendMembershipConfirmationCode`, { + recipientEmail: email, + confirmationCode: confirmCode + }) + .then(() => resolve(true)) + .catch(() => resolve(false)); + }); +} + +module.exports = { sendUnsubscribeEmail, sendVerificationEmail, sendPasswordReset, membershipConfirmationCode };