From 932b115ccef127cb780237dd3e2e4b075f9d3700 Mon Sep 17 00:00:00 2001 From: Caroline Chen <64424139+carolineychen8@users.noreply.github.com> Date: Sun, 10 Nov 2024 15:18:23 -0500 Subject: [PATCH] Task 9 - Backend (#6) * changes * createRequest function tried to make createRequest function * post route * deletechapter function --------- Co-authored-by: michaelfenggg Co-authored-by: kygchng <66804026+kygchng@users.noreply.github.com> --- .../controllers/birthdayRequest.controller.ts | 329 ++++++++++++++++++ server/src/controllers/chapter.controller.ts | 31 +- server/src/routes/birthdayRequest.route.ts | 23 ++ server/src/routes/routers.ts | 6 + .../src/services/birthdayRequest.service.ts | 119 +++++++ server/src/services/chapter.service.ts | 14 +- server/src/services/mail.service.ts | 47 ++- 7 files changed, 566 insertions(+), 3 deletions(-) diff --git a/server/src/controllers/birthdayRequest.controller.ts b/server/src/controllers/birthdayRequest.controller.ts index e69de29b..c5363f6d 100644 --- a/server/src/controllers/birthdayRequest.controller.ts +++ b/server/src/controllers/birthdayRequest.controller.ts @@ -0,0 +1,329 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable consistent-return */ +/* eslint-disable import/prefer-default-export */ +import express from 'express'; +import ApiError from '../util/apiError.ts'; +import StatusCode from '../util/statusCode.ts'; +import { + getAllRequestsByID, + updateRequestStatusByID, + getRequestById, + deleteRequestByID, + createBirthdayRequestByID, +} from '../services/birthdayRequest.service.ts'; +import { getChapterById } from '../services/chapter.service.ts'; +import { + emailRequestUpdate, + emailRequestDelete, +} from '../services/mail.service.ts'; +import { IBirthdayRequest } from '../models/birthdayRequest.model.ts'; +import { IChapter } from '../models/chapter.model.ts'; + +const getAllRequests = async ( + req: express.Request, + res: express.Response, + next: express.NextFunction, +) => { + const { id } = req.params; + if (!id) { + next(ApiError.missingFields(['id'])); + return; + } + return ( + getAllRequestsByID(id) + .then((requestsList) => { + res.status(StatusCode.OK).send(requestsList); + }) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .catch((e) => { + next(ApiError.internal('Unable to retrieve all requests')); + }) + ); +}; + +const updateRequestStatus = async ( + req: express.Request, + res: express.Response, + next: express.NextFunction, +) => { + // request id + const { id } = req.params; + if (!id) { + next(ApiError.missingFields(['id'])); + return; + } + const { updatedValue } = req.body; + if (!updatedValue) { + next(ApiError.missingFields(['updatedValue'])); + return; + } + if (updatedValue !== 'Approved' && updatedValue !== 'Delivered') { + next(ApiError.internal('Invalid input')); + return; + } + // get the chapter and partner agency emails + // get request object by it's id + const request: IBirthdayRequest | null = await getRequestById(id); + if (!request) { + next(ApiError.notFound(`Request with id ${id} does not exist`)); + return; + } + // get chapter email by chapter ID + const chapter: IChapter | null = await getChapterById(request.chapterId); + if (!chapter) { + next(ApiError.notFound(`Chapter does not exist`)); + return; + } + // get partner agency email and chapter email in the request object + const agencyEmail = request.agencyWorkerEmail; + const chapterEmail = chapter.email; + + return ( + updateRequestStatusByID(id, updatedValue) + .then(() => { + emailRequestUpdate(agencyEmail, updatedValue, request.childName) + .then(() => { + emailRequestUpdate(chapterEmail, updatedValue, request.childName) + .then(() => + res.status(StatusCode.CREATED).send({ + message: `Email has been sent to all parties.`, + }), + ) + .catch(() => { + next(ApiError.internal('Failed to send chapter email.')); + }); + }) + .catch(() => { + next(ApiError.internal('Failed to send agency update email.')); + }); + }) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .catch((e) => { + next(ApiError.internal('Unable to retrieve all requests')); + }) + ); +}; + +const deleteRequest = async ( + req: express.Request, + res: express.Response, + next: express.NextFunction, +) => { + // request id + const { id } = req.params; + if (!id) { + next(ApiError.missingFields(['id'])); + return; + } + // get the chapter and partner agency emails + // get request object by it's id + const request: IBirthdayRequest | null = await getRequestById(id); + if (!request) { + next(ApiError.notFound(`Request with id ${id} does not exist`)); + return; + } + // get chapter email by chapter ID + const chapter: IChapter | null = await getChapterById(request.chapterId); + if (!chapter) { + next(ApiError.notFound(`Chapter does not exist`)); + return; + } + // get partner agency email and chapter email in the request object + const agencyEmail = request.agencyWorkerEmail; + const chapterEmail = chapter.email; + + return ( + deleteRequestByID(id) + .then(() => { + emailRequestDelete(agencyEmail, request.childName) + .then(() => { + emailRequestDelete(chapterEmail, request.childName) + .then(() => + res.status(StatusCode.CREATED).send({ + message: `Email has been sent to all parties.`, + }), + ) + .catch(() => { + next(ApiError.internal('Failed to send chapter email.')); + }); + }) + .catch(() => { + next(ApiError.internal('Failed to send agency update email.')); + }); + }) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .catch((e) => { + next(ApiError.internal('Unable to delete request.')); + }) + ); +}; + +const createRequest = async ( + req: express.Request, + res: express.Response, + next: express.NextFunction, +) => { + const { + chapterId, + deadlineDate, + childBirthday, + childAge, + childName, + childGender, + childRace, + childInterests, + childAllergies, + allergyDetails, + giftSuggestions, + additionalInfo, + agencyWorkerName, + agencyOrganization, + agencyWorkerPhone, + agencyWorkerEmail, + isFirstReferral, + agreeFeedback, + requestedDate, + status, + deliveryDate, + } = req.body; + if (!chapterId || typeof chapterId === 'string') { + next(ApiError.notFound(`chapterId does not exist or is invalid`)); + return; + } + if (!deadlineDate || !(deadlineDate instanceof Date)) { + next(ApiError.notFound(`deadlineDate does not exist or is invalid`)); + return; + } + if (!childBirthday || !(childBirthday instanceof Date)) { + next(ApiError.notFound(`childBirthday does not exist or is invalid`)); + return; + } + if (!childAge || typeof childAge !== 'number') { + next(ApiError.notFound(`childAge does not exist or is invalid`)); + return; + } + if (!childName || typeof childName === 'string') { + next(ApiError.notFound(`childName does not exist or is invalid`)); + return; + } + if (!childGender || typeof childGender === 'string') { + next(ApiError.notFound(`childGender does not exist or is invalid`)); + return; + } + if (childGender !== 'Boy' && childGender !== 'Girl') { + next(ApiError.notFound(`childGender is invalid`)); + return; + } + if (!childRace || typeof childRace === 'string') { + next(ApiError.notFound(`childRace does not exist or is invalid`)); + return; + } + if ( + childRace !== 'White' && + childRace !== 'Black or African American' && + childRace !== 'Hispanic or Latino' && + childRace !== 'Native American or American Indian' && + childRace !== 'Asian / Pacific Islander' && + childRace !== 'Not Sure' + ) { + next(ApiError.notFound(`childRace is invalid`)); + return; + } + if (!childInterests || typeof childInterests === 'string') { + next(ApiError.notFound(`childInterests does not exist or is invalid`)); + return; + } + if (childAllergies !== true && childAllergies !== false) { + next(ApiError.notFound(`childAllergies does not exist or is invalid`)); + return; + } + if (!allergyDetails || typeof allergyDetails === 'string') { + next(ApiError.notFound(`allergyDetails does not exist or is invalid`)); + return; + } + if (!giftSuggestions || typeof giftSuggestions === 'string') { + next(ApiError.notFound(`giftSuggestions does not exist or is invalid`)); + return; + } + if (!additionalInfo || typeof additionalInfo === 'string') { + next(ApiError.notFound(`additionalInfo does not exist or is invalid`)); + return; + } + if (!agencyWorkerName || typeof agencyWorkerName === 'string') { + next(ApiError.notFound(`agencyWorkerName does not exist or is invalid`)); + return; + } + if (!agencyOrganization || typeof agencyOrganization === 'string') { + next(ApiError.notFound(`agencyOrganization does not exist or is invalid`)); + return; + } + if (!agencyWorkerPhone || typeof agencyWorkerPhone === 'string') { + next(ApiError.notFound(`agencyWorkerPhone does not exist or is invalid`)); + return; + } + if (!agencyWorkerEmail || typeof agencyWorkerEmail === 'string') { + next(ApiError.notFound(`agencyWorkerEmail does not exist or is invalid`)); + return; + } + const emailRegex = + /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/g; + if (!agencyWorkerEmail.match(emailRegex)) { + next(ApiError.badRequest('Invalid email')); + return; + } + + if (isFirstReferral !== true && isFirstReferral !== false) { + next(ApiError.notFound(`isFirstReferral does not exist or is invalid`)); + return; + } + if (agreeFeedback !== true && agreeFeedback !== false) { + next(ApiError.notFound(`agreeFeedback does not exist or is invalid`)); + return; + } + if (!requestedDate || !(requestedDate instanceof Date)) { + next(ApiError.notFound(`requestedDate does not exist or is invalid`)); + return; + } + if (!status || typeof status === 'string') { + next(ApiError.notFound(`status does not exist or is invalid`)); + return; + } + if (status !== 'Pending' && status !== 'Approved' && status !== 'Delivered') { + next(ApiError.notFound(`status is invalid`)); + return; + } + if (!deliveryDate || !(deliveryDate instanceof Date)) { + next(ApiError.notFound(`deliveryDate does not exist or is invalid`)); + return; + } + try { + const user = await createBirthdayRequestByID( + chapterId, + deadlineDate, + childBirthday, + childAge, + childName, + childGender, + childRace, + childInterests, + childAllergies, + allergyDetails, + giftSuggestions, + additionalInfo, + agencyWorkerName, + agencyOrganization, + agencyWorkerPhone, + agencyWorkerEmail, + isFirstReferral, + agreeFeedback, + requestedDate, + status, + deliveryDate, + ); + res.sendStatus(StatusCode.CREATED); + } catch (err) { + next(ApiError.internal('Unable to register user.')); + } +}; + +export { getAllRequests, updateRequestStatus, deleteRequest, createRequest }; diff --git a/server/src/controllers/chapter.controller.ts b/server/src/controllers/chapter.controller.ts index b190a4f6..fdd97abf 100644 --- a/server/src/controllers/chapter.controller.ts +++ b/server/src/controllers/chapter.controller.ts @@ -5,7 +5,10 @@ import StatusCode from '../util/statusCode.ts'; import { toggleRequestByID, getAllChaptersFromDB, + getChapterById, + deleteChapterByID, } from '../services/chapter.service.ts'; +import { IChapter } from '../models/chapter.model.ts'; const getAllChapters = async ( req: express.Request, @@ -45,4 +48,30 @@ const toggleRequest = async ( }); }; -export { toggleRequest, getAllChapters }; +const deleteChapter = async ( + req: express.Request, + res: express.Response, + next: express.NextFunction, +) => { + // request id + const { id } = req.params; + if (!id) { + next(ApiError.missingFields(['id'])); + return; + } + // get chapter email by chapter ID + const chapter: IChapter | null = await getChapterById(id); + if (!chapter) { + next(ApiError.notFound(`Chapter does not exist`)); + return; + } + deleteChapterByID(id); + try { + res.sendStatus(StatusCode.CREATED); + } + catch (err) { + next(ApiError.internal('Unable to register user.')); + } +}; + +export { toggleRequest, getAllChapters, deleteChapter }; diff --git a/server/src/routes/birthdayRequest.route.ts b/server/src/routes/birthdayRequest.route.ts index e69de29b..1af9ef45 100644 --- a/server/src/routes/birthdayRequest.route.ts +++ b/server/src/routes/birthdayRequest.route.ts @@ -0,0 +1,23 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import express from 'express'; +import { isAdmin } from '../controllers/admin.middleware.ts'; +import { + getAllRequests, + updateRequestStatus, + deleteRequest, + createRequest, +} from '../controllers/birthdayRequest.controller.ts'; +import { isAuthenticated } from '../controllers/auth.middleware.ts'; +import 'dotenv/config'; + +const router = express.Router(); + +router.get('/all/:id', isAuthenticated, isAdmin, getAllRequests); + +router.put('/updatestatus/:id', isAuthenticated, isAdmin, updateRequestStatus); + +router.delete('/deleterequest/:id', isAuthenticated, isAdmin, deleteRequest); + +router.post('/createrequest', createRequest); +// isAuthenticated, isAdmin, +export default router; diff --git a/server/src/routes/routers.ts b/server/src/routes/routers.ts index 806b9447..d4a46b4f 100644 --- a/server/src/routes/routers.ts +++ b/server/src/routes/routers.ts @@ -10,6 +10,7 @@ import { Router } from 'express'; import adminRouter from './admin.route.ts'; import authRouter from './auth.route.ts'; import chapterRouter from './chapter.route.ts'; +import birthdayRequestRouter from './birthdayRequest.route.ts'; const prefixToRouterMap: { prefix: string; router: Router }[] = [ { @@ -24,6 +25,11 @@ const prefixToRouterMap: { prefix: string; router: Router }[] = [ prefix: '/api/chapter', router: chapterRouter, }, + { + prefix: '/api/birthdayrequest', + router: birthdayRequestRouter, + }, + ]; export default prefixToRouterMap; diff --git a/server/src/services/birthdayRequest.service.ts b/server/src/services/birthdayRequest.service.ts index e69de29b..0e4717b0 100644 --- a/server/src/services/birthdayRequest.service.ts +++ b/server/src/services/birthdayRequest.service.ts @@ -0,0 +1,119 @@ +/* eslint-disable import/prefer-default-export */ +import { BirthdayRequest } from '../models/birthdayRequest.model.ts'; + +const removeSensitiveDataQuery = [ + '-password', + '-verificationToken', + '-resetPasswordToken', + '-resetPasswordTokenExpiryDate', +]; + +const getAllRequestsByID = async (id: string) => { + const requestsList = await BirthdayRequest.findOne({ id }) + .select(removeSensitiveDataQuery) + .exec(); + return requestsList; +}; + +const updateRequestStatusByID = async (id: string, updatedValue: string) => { + const updatedRequest = await BirthdayRequest.findByIdAndUpdate(id, [ + { $set: { status: updatedValue } }, + // { $eq: [updatedValue, '$status'] } + ]).exec(); + return updatedRequest; +}; + +const getRequestById = async (id: string) => { + const request = await BirthdayRequest.findById(id) + .select(removeSensitiveDataQuery) + .exec(); + return request; +}; + +const deleteRequestByID = async (id: string) => { + const request = await BirthdayRequest.findByIdAndDelete(id).exec(); + return request; +}; + +/** + * Creates a new birthdayrequest in the database. + * @param chapterId - id representing the chapter the bithdayrequest is associated with + * @param deadlineDate - TBD + * @param childBirthday - TBD + * @param childAge - TBD + * @param childName - TBD + * @param childGender - TBD + * @param childRace - TBD + * @param childInterests - TBD + * @param childAllergies - TBD + * @param allergyDetails - TBD + * @param giftSuggestions - TBD + * @param additionalInfo - TBD + * @param agencyWorkerName - TBD + * @param agencyOrganization - TBD + * @param agencyWorkerPhone - TBD + * @param agencyWorkerEmail - TBD + * @param isFirstReferral - TBD + * @param agreeFeedback - TBD + * @param requestedDate - TBD + * @param status - TBD + * @param deliveryDate - TBD + * @returns The created {@link BirthdayRequest} + */ +const createBirthdayRequestByID = async ( + chapterId: string, + deadlineDate: Date, + childBirthday: Date, + childAge: number, + childName: string, + childGender: string, + childRace: string, + childInterests: string, + childAllergies: boolean, + allergyDetails: string, + giftSuggestions: string, + additionalInfo: string, + agencyWorkerName: string, + agencyOrganization: string, + agencyWorkerPhone: string, + agencyWorkerEmail: string, + isFirstReferral: boolean, + agreeFeedback: boolean, + requestedDate: Date, + status: string, + deliveryDate: Date, +) => { + const newBirthdayRequest = new BirthdayRequest({ + chapterId, + deadlineDate, + childBirthday, + childAge, + childName, + childGender, + childRace, + childInterests, + childAllergies, + allergyDetails, + giftSuggestions, + additionalInfo, + agencyWorkerName, + agencyOrganization, + agencyWorkerPhone, + agencyWorkerEmail, + isFirstReferral, + agreeFeedback, + requestedDate, + status, + deliveryDate, + }); + const returnedBirthdayRequest = await newBirthdayRequest.save(); + return returnedBirthdayRequest; +}; + +export { + getAllRequestsByID, + updateRequestStatusByID, + getRequestById, + deleteRequestByID, + createBirthdayRequestByID, +}; diff --git a/server/src/services/chapter.service.ts b/server/src/services/chapter.service.ts index f06ab31c..7c5bf73b 100644 --- a/server/src/services/chapter.service.ts +++ b/server/src/services/chapter.service.ts @@ -22,4 +22,16 @@ const getAllChaptersFromDB = async () => { return userList; }; -export { toggleRequestByID, getAllChaptersFromDB }; +const getChapterById = async (id: string) => { + const chapter = await Chapter.findById(id) + .select(removeSensitiveDataQuery) + .exec(); + return chapter; +}; + +const deleteChapterByID = async (id: string) => { + const chapter = await Chapter.findByIdAndDelete(id).exec(); + return chapter; +} + +export { toggleRequestByID, getAllChaptersFromDB, getChapterById, deleteChapterByID }; diff --git a/server/src/services/mail.service.ts b/server/src/services/mail.service.ts index 1d35a313..9e7e5827 100644 --- a/server/src/services/mail.service.ts +++ b/server/src/services/mail.service.ts @@ -89,4 +89,49 @@ const emailInviteLink = async (email: string, token: string) => { await SGmail.send(mailSettings); }; -export { emailVerificationLink, emailResetPasswordLink, emailInviteLink }; +const emailRequestUpdate = async ( + email: string, + newStatus: string, + childName: string, +) => { + const mailSettings: MailDataRequired = { + from: { + email: process.env.SENDGRID_EMAIL_ADDRESS || 'missing@mail.com', + name: senderName, + }, + to: email, + subject: 'Birthday Box Request Status Update', + html: + `

Your birthday box request status for ${childName} has been changed. ` + + `

The new status of your request is ${newStatus}. ` + + `

If you did not request a birthday box, ` + + `please ignore this message.

`, + }; + // Send the email and propogate the error up if one exists + await SGmail.send(mailSettings); +}; + +const emailRequestDelete = async (email: string, childName: string) => { + const mailSettings: MailDataRequired = { + from: { + email: process.env.SENDGRID_EMAIL_ADDRESS || 'missing@mail.com', + name: senderName, + }, + to: email, + subject: 'Birthday Box Request Deleted', + html: + `

Your birthday box request for ${childName} has been deleted. ` + + `

If you did not request a birthday box, ` + + `please ignore this message.

`, + }; + // Send the email and propogate the error up if one exists + await SGmail.send(mailSettings); +}; + +export { + emailVerificationLink, + emailResetPasswordLink, + emailInviteLink, + emailRequestUpdate, + emailRequestDelete, +};