diff --git a/constants/requests.ts b/constants/requests.ts index e9d510012..b7fb33999 100644 --- a/constants/requests.ts +++ b/constants/requests.ts @@ -26,6 +26,8 @@ export const REQUEST_LOG_TYPE = { REQUEST_CANCELLED: "REQUEST_CANCELLED", REQUEST_UPDATED: "REQUEST_UPDATED", PENDING_REQUEST_FOUND: "PENDING_REQUEST_FOUND", + REQUEST_ALREADY_APPROVED: "REQUEST_ALREADY_APPROVED", + REQUEST_ALREADY_REJECTED: "REQUEST_ALREADY_REJECTED", }; export const REQUEST_CREATED_SUCCESSFULLY = "Request created successfully"; @@ -39,7 +41,9 @@ export const REQUEST_ALREADY_REJECTED = "Request already rejected"; export const ERROR_WHILE_FETCHING_REQUEST = "Error while fetching request"; export const ERROR_WHILE_CREATING_REQUEST = "Error while creating request"; export const ERROR_WHILE_UPDATING_REQUEST = "Error while updating request"; +export const ERROR_WHILE_ACKNOWLEDGING_REQUEST = "Error while acknowledging request"; +export const REQUEST_ID_REQUIRED = "Request id is required"; export const REQUEST_DOES_NOT_EXIST = "Request does not exist"; export const REQUEST_ALREADY_PENDING = "Request already exists please wait for approval or rejection"; export const UNAUTHORIZED_TO_CREATE_OOO_REQUEST = "Unauthorized to create OOO request"; diff --git a/controllers/oooRequests.ts b/controllers/oooRequests.ts index 36d2baeab..c2162d039 100644 --- a/controllers/oooRequests.ts +++ b/controllers/oooRequests.ts @@ -12,6 +12,9 @@ import { REQUEST_ALREADY_PENDING, USER_STATUS_NOT_FOUND, OOO_STATUS_ALREADY_EXIST, + UNAUTHORIZED_TO_UPDATE_REQUEST, + ERROR_WHILE_ACKNOWLEDGING_REQUEST, + REQUEST_ID_REQUIRED, } from "../constants/requests"; import { statusState } from "../constants/userStatus"; import { logType } from "../constants/logs"; @@ -20,17 +23,24 @@ import { getRequestByKeyValues, getRequests, updateRequest } from "../models/req import { createUserFutureStatus } from "../models/userFutureStatus"; import { getUserStatus, addFutureStatus } from "../models/userStatus"; import { createOooRequest, validateUserStatus } from "../services/oooRequest"; +import * as oooRequestService from "../services/oooRequest"; import { CustomResponse } from "../typeDefinitions/global"; -import { OooRequestCreateRequest, OooRequestResponse, OooStatusRequest } from "../types/oooRequest"; +import { + AcknowledgeOooRequest, + OooRequestCreateRequest, + OooRequestResponse, + OooStatusRequest, +} from "../types/oooRequest"; import { UpdateRequest } from "../types/requests"; +import { NextFunction } from "express"; /** * Controller to handle the creation of OOO requests. - * + * * This function processes the request to create an OOO request, * validates the user status, checks existing requests, * and stores the new request in the database with logging. - * + * * @param {OooRequestCreateRequest} req - The Express request object containing the body with OOO details. * @param {CustomResponse} res - The Express response object used to send back the response. * @returns {Promise} Resolves to a response with the success or an error message. @@ -39,7 +49,6 @@ export const createOooRequestController = async ( req: OooRequestCreateRequest, res: OooRequestResponse ): Promise => { - const requestBody = req.body; const { id: userId, username } = req.userData; const isUserPartOfDiscord = req.userData.roles.in_discord; @@ -57,25 +66,26 @@ export const createOooRequestController = async ( if (validationResponse) { if (validationResponse.error === USER_STATUS_NOT_FOUND) { - return res.boom.notFound(validationResponse.error); + return res.boom.notFound(validationResponse.error); } if (validationResponse.error === OOO_STATUS_ALREADY_EXIST) { - return res.boom.forbidden(validationResponse.error); + return res.boom.forbidden(validationResponse.error); } } const latestOooRequest: OooStatusRequest = await getRequestByKeyValues({ - userId, - type: REQUEST_TYPE.OOO, - status: REQUEST_STATE.PENDING, + requestedBy: userId, + type: REQUEST_TYPE.OOO, + status: REQUEST_STATE.PENDING, }); if (latestOooRequest) { - await addLog(logType.PENDING_REQUEST_FOUND, - { userId, oooRequestId: latestOooRequest.id }, - { message: REQUEST_ALREADY_PENDING } - ); - return res.boom.conflict(REQUEST_ALREADY_PENDING); + await addLog( + logType.PENDING_REQUEST_FOUND, + { userId, oooRequestId: latestOooRequest.id }, + { message: REQUEST_ALREADY_PENDING } + ); + return res.boom.conflict(REQUEST_ALREADY_PENDING); } await createOooRequest(requestBody, username, userId); @@ -103,7 +113,7 @@ export const updateOooRequestController = async (req: UpdateRequest, res: Custom return res.boom.badRequest(requestResult.error); } const [logType, returnMessage] = - requestResult.state === REQUEST_STATE.APPROVED + requestResult.status === REQUEST_STATE.APPROVED ? [REQUEST_LOG_TYPE.REQUEST_APPROVED, REQUEST_APPROVED_SUCCESSFULLY] : [REQUEST_LOG_TYPE.REQUEST_REJECTED, REQUEST_REJECTED_SUCCESSFULLY]; @@ -118,7 +128,7 @@ export const updateOooRequestController = async (req: UpdateRequest, res: Custom body: requestResult, }; await addLog(requestLog.type, requestLog.meta, requestLog.body); - if (requestResult.state === REQUEST_STATE.APPROVED) { + if (requestResult.status === REQUEST_STATE.APPROVED) { const requestData = await getRequests({ id: requestId }); if (requestData) { @@ -148,3 +158,54 @@ export const updateOooRequestController = async (req: UpdateRequest, res: Custom return res.boom.badImplementation(ERROR_WHILE_UPDATING_REQUEST); } }; + +/** + * Acknowledges an Out-of-Office (OOO) request. + * Devflag and superuser checks are handled by conditionalOooChecks middleware. + * + * @param {AcknowledgeOooRequest} req - The request object containing request parameters and user data + * @param {OooRequestResponse} res - The response object + * @param {NextFunction} next - Express next function for error handling + * @returns {Promise} The response object or void on error + */ +export const acknowledgeOooRequest = async ( + req: AcknowledgeOooRequest, + res: OooRequestResponse, + next: NextFunction +): Promise => { + try { + + const requestBody = req.body; + const superUserId = req.userData.id; + const requestId = req.params.id; + + if (!requestId) { + return res.boom.badRequest(REQUEST_ID_REQUIRED); + } + + const response = await oooRequestService.acknowledgeOooRequest(requestId, requestBody, superUserId); + + return res.status(200).json({ + message: response.message, + }); + } catch (error) { + logger.error(ERROR_WHILE_ACKNOWLEDGING_REQUEST, { + error: error.message, + requestId: req.params.id, + superUserId: req.userData?.id, + requestType: req.body?.type, + userAgent: req.get("User-Agent"), + timestamp: new Date().toISOString(), + }); + + + if (error.statusCode === 409) { + return res.boom.conflict(error.message); + } else if (error.statusCode === 400) { + return res.boom.badRequest(error.message); + } else { + + next(new Error(ERROR_WHILE_ACKNOWLEDGING_REQUEST)); + } + } +}; diff --git a/controllers/requests.ts b/controllers/requests.ts index fd8974ea0..0f31acb52 100644 --- a/controllers/requests.ts +++ b/controllers/requests.ts @@ -5,8 +5,8 @@ import { } from "../constants/requests"; import { getRequests } from "../models/requests"; import { getPaginatedLink } from "../utils/helper"; -import { createOooRequestController, updateOooRequestController } from "./oooRequests"; -import { OooRequestCreateRequest, OooRequestResponse } from "../types/oooRequest"; +import { acknowledgeOooRequest, createOooRequestController, updateOooRequestController } from "./oooRequests"; +import { AcknowledgeOooRequest, OooRequestCreateRequest, OooRequestResponse } from "../types/oooRequest"; import { CustomResponse } from "../typeDefinitions/global"; import { ExtensionRequestRequest, ExtensionRequestResponse } from "../types/extensionRequests"; import { createTaskExtensionRequest, updateTaskExtensionRequest } from "./extensionRequestsv2"; @@ -16,8 +16,7 @@ import { createTaskRequestController } from "./taskRequestsv2"; import { OnboardingExtensionCreateRequest, OnboardingExtensionResponse, UpdateOnboardingExtensionStateRequest } from "../types/onboardingExtension"; import { createOnboardingExtensionRequestController, updateOnboardingExtensionRequestController, updateOnboardingExtensionRequestState } from "./onboardingExtension"; import { UpdateOnboardingExtensionRequest } from "../types/onboardingExtension"; - -import { Request } from "express"; +import { NextFunction, Request } from "express"; export const createRequestController = async ( req: OooRequestCreateRequest | ExtensionRequestRequest | TaskRequestRequest | OnboardingExtensionCreateRequest, @@ -67,10 +66,10 @@ export const getRequestsController = async (req: any, res: any) => { }); } - const { allRequests, next, prev, page } = requests; - if (allRequests.length === 0) { - return res.status(204).send(); - } + const { allRequests, next, prev, page } = requests; + if (allRequests.length === 0) { + return res.status(204).send(); + } if (page) { const pageLink = `/requests?page=${page}`; @@ -108,6 +107,7 @@ export const getRequestsController = async (req: any, res: any) => { next: nextUrl, prev: prevUrl, }); + } catch (err) { logger.error(ERROR_WHILE_FETCHING_REQUEST, err); return res.boom.badImplementation(ERROR_WHILE_FETCHING_REQUEST); @@ -121,9 +121,13 @@ export const getRequestsController = async (req: any, res: any) => { * @param {CustomResponse} res - The response object. * @returns {Promise} Resolves or sends an error for invalid types. */ -export const updateRequestBeforeAcknowledgedController = async (req: Request, res: CustomResponse) => { +export const updateRequestBeforeAcknowledgedController = async (req: Request, res: CustomResponse, next: NextFunction) => { const type = req.body.type; + switch(type){ + case REQUEST_TYPE.OOO: + await acknowledgeOooRequest(req as AcknowledgeOooRequest, res as OooRequestResponse, next); + break; case REQUEST_TYPE.ONBOARDING: await updateOnboardingExtensionRequestController(req as UpdateOnboardingExtensionRequest, res as OnboardingExtensionResponse); break; diff --git a/middlewares/conditionalOooChecks.ts b/middlewares/conditionalOooChecks.ts new file mode 100644 index 000000000..813d7e3c3 --- /dev/null +++ b/middlewares/conditionalOooChecks.ts @@ -0,0 +1,32 @@ +import { NextFunction } from "express"; +import { CustomRequest, CustomResponse } from "../types/global"; +import { REQUEST_TYPE } from "../constants/requests"; +import { devFlagMiddleware } from "./devFlag"; +import authorizeRoles from "./authorizeRoles"; +const { SUPERUSER } = require("../constants/roles"); + +/** + * Conditional middleware that applies devFlag and superuser checks only for OOO requests. + * This allows onboarding requests to bypass these checks while maintaining security for OOO operations. + * + * @param {CustomRequest} req - The request object + * @param {CustomResponse} res - The response object + * @param {NextFunction} next - The next middleware function + */ +export const conditionalOooChecks = (req: CustomRequest, res: CustomResponse, next: NextFunction) => { + const requestType = req.body?.type; + + + if (requestType === REQUEST_TYPE.OOO) { + + devFlagMiddleware(req, res, (err: any) => { + if (err) return next(err); + + + authorizeRoles([SUPERUSER])(req, res, next); + }); + } else { + + next(); + } +}; diff --git a/middlewares/validators/oooRequests.ts b/middlewares/validators/oooRequests.ts index ab73929f1..5bf4ccda7 100644 --- a/middlewares/validators/oooRequests.ts +++ b/middlewares/validators/oooRequests.ts @@ -1,7 +1,7 @@ import joi from "joi"; import { NextFunction } from "express"; import { REQUEST_STATE, REQUEST_TYPE } from "../../constants/requests"; -import { OooRequestCreateRequest, OooRequestResponse } from "../../types/oooRequest"; +import { AcknowledgeOooRequest, OooRequestCreateRequest, OooRequestResponse } from "../../types/oooRequest"; export const createOooStatusRequestValidator = async ( req: OooRequestCreateRequest, @@ -38,3 +38,47 @@ export const createOooStatusRequestValidator = async ( await schema.validateAsync(req.body, { abortEarly: false }); }; + +const schema = joi + .object() + .strict() + .keys({ + comment: joi.string().optional() + .messages({ + "string.empty": "comment cannot be empty", + }), + status: joi + .string() + .valid(REQUEST_STATE.APPROVED, REQUEST_STATE.REJECTED) + .required() + .messages({ + "any.only": "status must be APPROVED or REJECTED", + }), + type: joi.string().equal(REQUEST_TYPE.OOO).required().messages({ + "any.required": "type is required", + "any.only": "type must be OOO" + }) + }); + +/** + * Middleware to validate the acknowledge Out-Of-Office (OOO) request payload. + * + * @param {AcknowledgeOooRequest} req - The request object containing the body to be validated. + * @param {OooRequestResponse} res - The response object used to send error responses if validation fails. + * @param {NextFunction} next - The next middleware function to call if validation succeeds. + * @returns {Promise} Resolves or sends errors. + */ +export const acknowledgeOooRequest = async ( + req: AcknowledgeOooRequest, + res: OooRequestResponse, + next: NextFunction +): Promise => { + try { + await schema.validateAsync(req.body, { abortEarly: false }); + next(); + } catch (error) { + const errorMessages = error.details.map((detail) => detail.message); + logger.error(`Error while validating request payload : ${errorMessages}`); + return res.boom.badRequest(errorMessages); + } +}; diff --git a/middlewares/validators/requests.ts b/middlewares/validators/requests.ts index 80ff0478b..af9386994 100644 --- a/middlewares/validators/requests.ts +++ b/middlewares/validators/requests.ts @@ -1,8 +1,8 @@ import joi from "joi"; import { NextFunction } from "express"; import { REQUEST_STATE, REQUEST_TYPE } from "../../constants/requests"; -import { OooRequestCreateRequest, OooRequestResponse } from "../../types/oooRequest"; -import { createOooStatusRequestValidator } from "./oooRequests"; +import { AcknowledgeOooRequest, OooRequestCreateRequest, OooRequestResponse } from "../../types/oooRequest"; +import { acknowledgeOooRequest, createOooStatusRequestValidator } from "./oooRequests"; import { createExtensionRequestValidator } from "./extensionRequestsv2"; import {createTaskRequestValidator} from "./taskRequests"; import { ExtensionRequestRequest, ExtensionRequestResponse } from "../../types/extensionRequests"; @@ -92,6 +92,10 @@ export const getRequestsMiddleware = async (req: OooRequestCreateRequest, res: O .string() .valid(REQUEST_STATE.APPROVED, REQUEST_STATE.PENDING, REQUEST_STATE.REJECTED) .optional(), + status: joi + .string() + .valid(REQUEST_STATE.APPROVED, REQUEST_STATE.PENDING, REQUEST_STATE.REJECTED) + .optional(), page: joi.number().integer().min(0).when("next", { is: joi.exist(), then: joi.forbidden().messages({ @@ -125,18 +129,24 @@ export const getRequestsMiddleware = async (req: OooRequestCreateRequest, res: O /** * Validates update requests based on their type. * - * @param {UpdateOnboardingExtensionRequest} req - Request object. + * @param {UpdateOnboardingExtensionRequest | AcknowledgeOooRequest} req - Request object. * @param {CustomResponse} res - Response object. * @param {NextFunction} next - Next middleware if valid. * @returns {Promise} Resolves or sends errors. */ export const updateRequestValidator = async ( - req: UpdateOnboardingExtensionRequest, + req: UpdateOnboardingExtensionRequest | AcknowledgeOooRequest, res: CustomResponse, next: NextFunction ): Promise => { const type = req.body.type; + switch (type) { + case REQUEST_TYPE.OOO: + await acknowledgeOooRequest( + req, + res as OooRequestResponse, next); + break; case REQUEST_TYPE.ONBOARDING: await updateOnboardingExtensionRequestValidator( req, @@ -145,4 +155,4 @@ export const updateRequestValidator = async ( default: return res.boom.badRequest("Invalid type"); } -}; \ No newline at end of file +}; diff --git a/models/requests.ts b/models/requests.ts index 064eebd8c..28fa6779f 100644 --- a/models/requests.ts +++ b/models/requests.ts @@ -1,6 +1,6 @@ import firestore from "../utils/firestore"; const requestModel = firestore.collection("requests"); -import { REQUEST_ALREADY_APPROVED, REQUEST_ALREADY_REJECTED, REQUEST_STATE } from "../constants/requests"; +import { REQUEST_ALREADY_APPROVED, REQUEST_ALREADY_REJECTED, REQUEST_STATE, REQUEST_TYPE } from "../constants/requests"; import { ERROR_WHILE_FETCHING_REQUEST, ERROR_WHILE_CREATING_REQUEST, @@ -8,6 +8,7 @@ import { REQUEST_DOES_NOT_EXIST, } from "../constants/requests"; import { getUserId } from "../utils/users"; +import { NotFound } from "http-errors"; const SIZE = 5; export const createRequest = async (body: any) => { @@ -29,7 +30,7 @@ export const createRequest = async (body: any) => { } }; -export const updateRequest = async (id: string, body: any, lastModifiedBy: string, type:string) => { +export const updateRequest = async (id: string, body: any, lastModifiedBy: string, type: string) => { try { const existingRequestDoc = await requestModel.doc(id).get(); if (!existingRequestDoc.exists) { @@ -37,12 +38,17 @@ export const updateRequest = async (id: string, body: any, lastModifiedBy: strin error: REQUEST_DOES_NOT_EXIST, }; } - if (existingRequestDoc.data().state === REQUEST_STATE.APPROVED) { + + + const statusField = type === REQUEST_TYPE.OOO ? 'status' : 'state'; + const currentStatus = existingRequestDoc.data()[statusField]; + + if (currentStatus === REQUEST_STATE.APPROVED) { return { error: REQUEST_ALREADY_APPROVED, }; } - if (existingRequestDoc.data().state === REQUEST_STATE.REJECTED) { + if (currentStatus === REQUEST_STATE.REJECTED) { return { error: REQUEST_ALREADY_REJECTED, }; @@ -56,9 +62,33 @@ export const updateRequest = async (id: string, body: any, lastModifiedBy: strin const requestBody: any = { updatedAt: Date.now(), lastModifiedBy, - ...body, }; + + + if (type === REQUEST_TYPE.OOO && body.state !== undefined) { + requestBody.status = body.state; + + Object.keys(body).forEach(key => { + if (key !== 'state') { + requestBody[key] = body[key]; + } + }); + } else { + + Object.assign(requestBody, body); + } + await requestModel.doc(id).update(requestBody); + + + if (type === REQUEST_TYPE.OOO && body.state !== undefined) { + return { + id, + state: body.state, + ...requestBody, + }; + } + return { id, ...requestBody, @@ -69,6 +99,24 @@ export const updateRequest = async (id: string, body: any, lastModifiedBy: strin } }; +export const getRequestById = async (id: string): Promise<{ id: string; [key: string]: any }> => { + try { + const requestDoc = await requestModel.doc(id).get(); + + if (!requestDoc.exists) { + throw new NotFound(REQUEST_DOES_NOT_EXIST); + } + + return { + id: requestDoc.id, + ...requestDoc.data() + }; + } catch (error) { + logger.error(ERROR_WHILE_FETCHING_REQUEST, error); + throw error; + } +}; + export const getRequests = async (query: any) => { let { id, type, requestedBy, state, prev, next, page, size = SIZE } = query; const dev = query.dev === "true"; @@ -79,20 +127,19 @@ export const getRequests = async (query: any) => { let requestQuery: any = requestModel; if (id) { - const requestDoc = await requestModel.doc(id).get(); - if (!requestDoc.exists) { - return null; + try { + return await getRequestById(id); + } catch (error) { + if (error.message === REQUEST_DOES_NOT_EXIST) { + return null; + } + throw error; } - return { - id: requestDoc.id, - ...requestDoc.data(), - }; } - - if(requestedBy && dev){ + + if (requestedBy && dev) { requestQuery = requestQuery.where("requestedBy", "==", requestedBy); - } - else if (requestedBy) { + } else if (requestedBy) { const requestedByUserId = await getUserId(requestedBy); requestQuery = requestQuery.where("requestedBy", "==", requestedByUserId); } @@ -101,7 +148,18 @@ export const getRequests = async (query: any) => { requestQuery = requestQuery.where("type", "==", type); } if (state) { - requestQuery = requestQuery.where("state", "==", state); + const fieldName = type === REQUEST_TYPE.OOO ? 'status' : 'state'; + requestQuery = requestQuery.where(fieldName, "==", state); + } + + // Handle status field for OOO requests + if (query.status && type === REQUEST_TYPE.OOO) { + requestQuery = requestQuery.where("status", "==", query.status); + } + + // Ensure OOO requests are properly filtered when type is specified + if (type === REQUEST_TYPE.OOO && state) { + requestQuery = requestQuery.where("status", "==", state); } requestQuery = requestQuery.orderBy("createdAt", "desc"); @@ -149,6 +207,56 @@ export const getRequests = async (query: any) => { return null; } + // todo: remove this once previous OOO requests are removed form the database + // @ankush and @suraj had a discussion to manually update or remove the previous OOO requests + if (type === REQUEST_TYPE.OOO) { + const oooRequests = []; + if (!dev) { + for (const request of allRequests) { + if (request.status) { + const modifiedRequest = { + id: request.id, + type: request.type, + from: request.from, + until: request.until, + message: request.reason, + state: request.status, + lastModifiedBy: request.lastModifiedBy ?? "", + requestedBy: request.userId, + reason: request.comment ?? "", + createdAt: request.createdAt, + updatedAt: request.updatedAt, + }; + oooRequests.push(modifiedRequest); + } else { + oooRequests.push(request); + } + } + } else { + for (const request of allRequests) { + + if (request.status) { + const modifiedRequest = { + id: request.id, + type: request.type, + from: request.from, + until: request.until, + reason: request.message, + state: request.status, // Map status to state for consistent API + lastModifiedBy: request.lastModifiedBy ?? null, + requestedBy: request.requestedBy, + comment: request.reason ?? null, + createdAt: request.createdAt, + updatedAt: request.updatedAt, + }; + oooRequests.push(modifiedRequest); + } else { + oooRequests.push(request); + } + } + } + allRequests = oooRequests; + } return { allRequests, prev: prevDoc.empty ? null : prevDoc.docs[0].id, @@ -190,4 +298,3 @@ export const getRequestByKeyValues = async (keyValues: KeyValues) => { throw error; } }; - diff --git a/routes/requests.ts b/routes/requests.ts index 098e00a82..b57c5e78b 100644 --- a/routes/requests.ts +++ b/routes/requests.ts @@ -3,6 +3,7 @@ const router = express.Router(); const authorizeRoles = require("../middlewares/authorizeRoles"); const { SUPERUSER } = require("../constants/roles"); import authenticate from "../middlewares/authenticate"; +import { conditionalOooChecks } from "../middlewares/conditionalOooChecks"; import { createRequestsMiddleware, updateRequestsMiddleware, @@ -18,10 +19,9 @@ import { import { skipAuthenticateForOnboardingExtensionRequest } from "../middlewares/skipAuthenticateForOnboardingExtension"; import { verifyDiscordBot } from "../middlewares/authorizeBot"; - router.get("/", getRequestsMiddleware, getRequestsController); router.post("/", skipAuthenticateForOnboardingExtensionRequest(authenticate, verifyDiscordBot), createRequestsMiddleware, createRequestController); router.put("/:id",authenticate, authorizeRoles([SUPERUSER]), updateRequestsMiddleware, updateRequestController); -router.patch("/:id", authenticate, updateRequestValidator, updateRequestBeforeAcknowledgedController); +router.patch("/:id", authenticate, conditionalOooChecks, updateRequestValidator, updateRequestBeforeAcknowledgedController); module.exports = router; diff --git a/services/oooRequest.ts b/services/oooRequest.ts index 2e2f35ab7..21c390f11 100644 --- a/services/oooRequest.ts +++ b/services/oooRequest.ts @@ -5,13 +5,23 @@ import { REQUEST_LOG_TYPE, REQUEST_STATE, USER_STATUS_NOT_FOUND, + REQUEST_TYPE, + REQUEST_ALREADY_APPROVED, + REQUEST_ALREADY_REJECTED, + REQUEST_APPROVED_SUCCESSFULLY, + REQUEST_REJECTED_SUCCESSFULLY, + INVALID_REQUEST_TYPE, } from "../constants/requests"; -import { userState } from "../constants/userStatus"; -import { createRequest } from "../models/requests"; +import { statusState, userState } from "../constants/userStatus"; +import { createRequest, getRequestById } from "../models/requests"; import { OooStatusRequest, OooStatusRequestBody } from "../types/oooRequest"; import { UserStatus } from "../types/userStatus"; import { addLog } from "./logService"; - +import { BadRequest, Conflict } from "http-errors"; +import { updateRequest } from "../models/requests"; +import { AcknowledgeOooRequestBody } from "../types/oooRequest"; +import { addFutureStatus } from "../models/userStatus"; +import { createUserFutureStatus } from "../models/userFutureStatus"; /** * Validates the user status. * @@ -66,8 +76,7 @@ export const createOooRequest = async ( from: body.from, until: body.until, type: body.type, - requestedBy: username, - userId, + requestedBy: userId, reason: body.reason, comment: null, status: REQUEST_STATE.PENDING, @@ -93,3 +102,111 @@ export const createOooRequest = async ( throw error; } } + +/** + * Validates an Out-Of-Office (OOO) acknowledge request + * + * @param {string} requestType - The type of the request (expected to be 'OOO'). + * @param {string} requestStatus - The current status of the request. + * @throws {Error} Throws an error if an issue occurs during validation. + */ +export const validateOooAcknowledgeRequest = async ( + requestType: string, + requestStatus: string, +) => { + + try { + + if (requestType !== REQUEST_TYPE.OOO) { + throw new BadRequest(INVALID_REQUEST_TYPE); + } + + if (requestStatus === REQUEST_STATE.APPROVED) { + throw new Conflict(REQUEST_ALREADY_APPROVED); + } + + if (requestStatus === REQUEST_STATE.REJECTED) { + throw new Conflict(REQUEST_ALREADY_REJECTED); + } + } catch (error) { + logger.error("Error while validating OOO acknowledge request", error); + throw error; + } +} + +/** + * Acknowledges an Out-of-Office (OOO) request + * + * @param {string} requestId - The ID of the OOO request to acknowledge. + * @param {AcknowledgeOooRequestBody} body - The acknowledgement body containing acknowledging details. + * @param {string} superUserId - The unique identifier of the superuser user. + * @returns {Promise} The acknowledged OOO request. + * @throws {Error} Throws an error if an issue occurs during acknowledgment process. + */ +export const acknowledgeOooRequest = async ( + requestId: string, + body: AcknowledgeOooRequestBody, + superUserId: string, +) => { + try { + const requestData = await getRequestById(requestId); + + await validateOooAcknowledgeRequest(requestData.type, requestData.status); + + const requestResult = await updateRequest(requestId, body, superUserId, REQUEST_TYPE.OOO); + + if (requestResult.error) { + throw new BadRequest(requestResult.error); + } + + const [acknowledgeLogType, returnMessage] = + requestResult.status === REQUEST_STATE.APPROVED + ? [REQUEST_LOG_TYPE.REQUEST_APPROVED, REQUEST_APPROVED_SUCCESSFULLY] + : [REQUEST_LOG_TYPE.REQUEST_REJECTED, REQUEST_REJECTED_SUCCESSFULLY]; + + const requestLog = { + type: acknowledgeLogType, + meta: { + requestId, + action: LOG_ACTION.UPDATE, + userId: superUserId, + createdAt: Date.now(), + }, + body: requestResult, + }; + + await addLog(requestLog.type, requestLog.meta, requestLog.body); + + if (requestResult.status === REQUEST_STATE.APPROVED) { + await addFutureStatus({ + requestId, + state: REQUEST_TYPE.OOO, + from: requestData.from, + endsOn: requestData.until, + userId: requestData.requestedBy, + message: body.comment, + }); + await createUserFutureStatus({ + requestId, + status: userState.OOO, + state: statusState.UPCOMING, + from: requestData.from, + endsOn: requestData.until, + userId: requestData.requestedBy, + message: body.comment, + createdAt: Date.now() + }); + } + + return { + message: returnMessage, + data: { + id: requestResult.id, + ...requestResult, + }, + }; + } catch (error) { + logger.error("Error while acknowledging OOO request", error); + throw error; + } +} \ No newline at end of file diff --git a/test/fixtures/oooRequest/oooRequest.ts b/test/fixtures/oooRequest/oooRequest.ts index 30b72d2a0..d65080b2b 100644 --- a/test/fixtures/oooRequest/oooRequest.ts +++ b/test/fixtures/oooRequest/oooRequest.ts @@ -7,9 +7,10 @@ export const createOooStatusRequests = { from: Date.now() + 100000, until: Date.now() + 200000, message: "Out of office for personal reasons.", - state: REQUEST_STATE.PENDING, + status: REQUEST_STATE.PENDING, createdAt: 1234567890, updatedAt: 1234567890, + dev: "true", }; export const validOooStatusRequests = { @@ -27,8 +28,7 @@ export const createdOOORequest = { reason: validOooStatusRequests.reason, status: "PENDING", lastModifiedBy: null, - requestedBy: "suraj-maity-1", - userId: "jCqqOYCnm93mcmaYuSsQ", + requestedBy: "jCqqOYCnm93mcmaYuSsQ", comment: null }; @@ -87,7 +87,8 @@ export const createOooRequests = { from: Date.now() + 100000, until: Date.now() + 200000, message: "Out of office for personal reasons.", - state: REQUEST_STATE.PENDING, + status: REQUEST_STATE.PENDING, + dev: "true", }; export const createOooRequests2 = { requestedBy: "testUser2", @@ -95,7 +96,8 @@ export const createOooRequests2 = { from: Date.now() + 100000, until: Date.now() + 200000, message: "Out of office for personal reasons.", - state: REQUEST_STATE.PENDING, + status: REQUEST_STATE.PENDING, + dev: "true", }; diff --git a/test/integration/requests.test.ts b/test/integration/requests.test.ts index 2e83acf18..7937ae28e 100644 --- a/test/integration/requests.test.ts +++ b/test/integration/requests.test.ts @@ -160,18 +160,32 @@ describe("/requests OOO", function () { }); it("should return 500 response when creating OOO request fails", function (done) { - sinon.stub(requestsQuery, "createRequest") - .throws("Error while creating OOO request"); - chai.request(app) - .post(requestsEndpoint) - .set("cookie", `${cookieName}=${authToken}`) - .send(validOooStatusRequests) - .end(function (err, res) { - if (err) return done(err); - expect(res.statusCode).to.equal(500); - expect(res.body.message).to.equal("An internal server error occurred"); - done(); - }); + + addUser(userData[17]).then(async (freshUserId) => { + + const testUserStatus = { + currentStatus: { + state: userState.ACTIVE + } + }; + await updateUserStatus(freshUserId, testUserStatus); + + const freshAuthToken = authService.generateAuthToken({ userId: freshUserId }); + + sinon.stub(requestsQuery, "createRequest") + .throws("Error while creating OOO request"); + + chai.request(app) + .post(requestsEndpoint) + .set("cookie", `${cookieName}=${freshAuthToken}`) + .send(validOooStatusRequests) + .end(function (err, res) { + if (err) return done(err); + expect(res.statusCode).to.equal(500); + expect(res.body.message).to.equal("An internal server error occurred"); + done(); + }); + }).catch(done); }); it("should create a new request when dev is true", function (done) { @@ -188,7 +202,7 @@ describe("/requests OOO", function () { expect(res.body).to.not.have.property("data"); await requestsQuery.getRequestByKeyValues({ - userId: testUserId, + requestedBy: testUserId, type: REQUEST_TYPE.OOO, status: REQUEST_STATE.PENDING }).then((request) => { @@ -587,7 +601,7 @@ describe("/requests OOO", function () { expect(res.body.data[0]).to.have.property("id"); expect(res.body.data[0]).to.have.property("requestedBy"); expect(res.body.data[0]).to.have.property("type"); - expect(res.body.data[0]).to.have.property("state"); + expect(res.body.data[0]).to.have.property("status"); expect(res.body.data[0]).to.have.property("message"); done(); }); @@ -615,13 +629,13 @@ describe("/requests OOO", function () { }); }); - it("should return all requests by specific user and state", function (done) { + it("should return all requests by specific user and status", function (done) { chai .request(app) - .get(`/requests?state=APPROVED&requestedBy=${userData[16].username}`) + .get(`/requests?status=APPROVED&requestedBy=${userData[16].username}`) .end(function (err, res) { expect(res).to.have.status(200); - expect(res.body.data.every((e: any) => e.state === "APPROVED")); + expect(res.body.data.every((e: any) => e.status === "APPROVED")); expect(res.body.data.every((e: any) => e.requestedBy === testUserId)); done(); }); diff --git a/test/unit/middlewares/oooRequests.test.ts b/test/unit/middlewares/oooRequests.test.ts index 11272e860..93840b7b8 100644 --- a/test/unit/middlewares/oooRequests.test.ts +++ b/test/unit/middlewares/oooRequests.test.ts @@ -128,4 +128,4 @@ describe("OOO Status Request Validators", function () { expect(nextSpy.calledOnce).to.be.true; }); }); -}); +}); \ No newline at end of file diff --git a/test/unit/models/requests.test.ts b/test/unit/models/requests.test.ts index 954024c86..10a058ed6 100644 --- a/test/unit/models/requests.test.ts +++ b/test/unit/models/requests.test.ts @@ -115,14 +115,14 @@ describe("models/oooRequests", () => { it("Should return a list of all the requests with specified state - APPROVED", async () => { const oooRequest: any = await createRequest(createOooStatusRequests); await updateRequest(oooRequest.id, updateOooApprovedRequests, updateOooApprovedRequests.lastModifiedBy, REQUEST_TYPE.OOO) - const query = { dev: "true", state: REQUEST_STATE.APPROVED }; + const query = { dev: "true", type: REQUEST_TYPE.OOO, state: REQUEST_STATE.APPROVED }; const oooRequestData = await getRequests(query); expect(oooRequestData.allRequests[0].state).to.be.equal(REQUEST_STATE.APPROVED); }); it("Should return a list of all the requests with specified state - PENDING", async () => { - await createRequest(createOooStatusRequests); - const query = { dev: "true", state: REQUEST_STATE.PENDING }; + const oooRequest = await createRequest(createOooStatusRequests); + const query = { dev: "true", type: REQUEST_TYPE.OOO, state: REQUEST_STATE.PENDING }; const oooRequestData = await getRequests(query); expect(oooRequestData.allRequests[0].state).to.be.equal(REQUEST_STATE.PENDING); }); @@ -144,7 +144,7 @@ describe("models/oooRequests", () => { }); it("Should return empty array if no data is found", async () => { - const query = { dev: "true", state: REQUEST_STATE.PENDING }; + const query = { dev: "true", type: REQUEST_TYPE.OOO, state: REQUEST_STATE.PENDING }; const oooRequestData = await getRequests(query); expect(oooRequestData).to.be.equal(null); }); @@ -179,4 +179,4 @@ describe("models/oooRequests", () => { expect(oooRequestData).to.be.equal(null); }); }); -}); +}); \ No newline at end of file diff --git a/test/unit/services/oooRequest.test.ts b/test/unit/services/oooRequest.test.ts index eb63242b6..4dda4dbe0 100644 --- a/test/unit/services/oooRequest.test.ts +++ b/test/unit/services/oooRequest.test.ts @@ -95,8 +95,8 @@ describe("Test OOO Request Service", function() { expect(response).to.deep.include({ ...createdOOORequest, id: response.id, - requestedBy:testUserName, - userId: testUserId + requestedBy: testUserId, + }); }); diff --git a/types/oooRequest.d.ts b/types/oooRequest.d.ts index 6b1c282a8..865daeb94 100644 --- a/types/oooRequest.d.ts +++ b/types/oooRequest.d.ts @@ -42,3 +42,20 @@ export type OooRequestCreateRequest = Request & { }; export type OooRequestUpdateRequest = Request & { oooRequestUpdateBody , userData: userData , query: RequestQuery , params: RequestParams }; + +export type AcknowledgeOooRequestQuery = RequestQuery & { + dev?: string +}; + +export type AcknowledgeOooRequestBody = { + type: REQUEST_TYPE.OOO; + comment?: string; + status: REQUEST_STATE.APPROVED | REQUEST_STATE.REJECTED; +} + +export type AcknowledgeOooRequest = Request & { + body: AcknowledgeOooRequestBody; + userData: userData; + query: AcknowledgeOooRequestQuery; + params: RequestParams; +}