Skip to content

Commit a30d990

Browse files
committed
feat: implement OOO approve/reject functionality
- Add OOO request approval/rejection logic - Update request validation and error handling - Add tests for OOO approval workflow
1 parent cd129fb commit a30d990

File tree

13 files changed

+403
-122
lines changed

13 files changed

+403
-122
lines changed

constants/requests.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export const REQUEST_LOG_TYPE = {
2626
REQUEST_CANCELLED: "REQUEST_CANCELLED",
2727
REQUEST_UPDATED: "REQUEST_UPDATED",
2828
PENDING_REQUEST_FOUND: "PENDING_REQUEST_FOUND",
29+
REQUEST_ALREADY_APPROVED: "REQUEST_ALREADY_APPROVED",
30+
REQUEST_ALREADY_REJECTED: "REQUEST_ALREADY_REJECTED",
2931
};
3032

3133
export const REQUEST_CREATED_SUCCESSFULLY = "Request created successfully";
@@ -39,7 +41,9 @@ export const REQUEST_ALREADY_REJECTED = "Request already rejected";
3941
export const ERROR_WHILE_FETCHING_REQUEST = "Error while fetching request";
4042
export const ERROR_WHILE_CREATING_REQUEST = "Error while creating request";
4143
export const ERROR_WHILE_UPDATING_REQUEST = "Error while updating request";
44+
export const ERROR_WHILE_ACKNOWLEDGING_REQUEST = "Error while acknowledging request";
4245

46+
export const REQUEST_ID_REQUIRED = "Request id is required";
4347
export const REQUEST_DOES_NOT_EXIST = "Request does not exist";
4448
export const REQUEST_ALREADY_PENDING = "Request already exists please wait for approval or rejection";
4549
export const UNAUTHORIZED_TO_CREATE_OOO_REQUEST = "Unauthorized to create OOO request";

controllers/oooRequests.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import {
1212
REQUEST_ALREADY_PENDING,
1313
USER_STATUS_NOT_FOUND,
1414
OOO_STATUS_ALREADY_EXIST,
15+
UNAUTHORIZED_TO_UPDATE_REQUEST,
16+
ERROR_WHILE_ACKNOWLEDGING_REQUEST,
17+
REQUEST_ID_REQUIRED,
1518
} from "../constants/requests";
1619
import { statusState } from "../constants/userStatus";
1720
import { logType } from "../constants/logs";
@@ -20,9 +23,11 @@ import { getRequestByKeyValues, getRequests, updateRequest } from "../models/req
2023
import { createUserFutureStatus } from "../models/userFutureStatus";
2124
import { getUserStatus, addFutureStatus } from "../models/userStatus";
2225
import { createOooRequest, validateUserStatus } from "../services/oooRequest";
26+
import * as oooRequestService from "../services/oooRequest";
2327
import { CustomResponse } from "../typeDefinitions/global";
24-
import { OooRequestCreateRequest, OooRequestResponse, OooStatusRequest } from "../types/oooRequest";
28+
import { AcknowledgeOooRequest, OooRequestCreateRequest, OooRequestResponse, OooStatusRequest } from "../types/oooRequest";
2529
import { UpdateRequest } from "../types/requests";
30+
import { NextFunction } from "express";
2631

2732
/**
2833
* Controller to handle the creation of OOO requests.
@@ -148,3 +153,46 @@ export const updateOooRequestController = async (req: UpdateRequest, res: Custom
148153
return res.boom.badImplementation(ERROR_WHILE_UPDATING_REQUEST);
149154
}
150155
};
156+
157+
/**
158+
* Acknowledges an Out-of-Office (OOO) request
159+
*
160+
* @param {AcknowledgeOooRequest} req - The request object.
161+
* @param {OooRequestResponse} res - The response object.
162+
* @returns {Promise<OooRequestResponse>} Resolves with success or failure.
163+
*/
164+
export const acknowledgeOooRequest = async (
165+
req: AcknowledgeOooRequest,
166+
res: OooRequestResponse,
167+
next: NextFunction
168+
)
169+
: Promise<OooRequestResponse> => {
170+
try {
171+
const dev = req.query.dev === "true";
172+
if(!dev) return res.boom.notImplemented("Feature not implemented");
173+
174+
const isSuperuser = req.userData?.roles?.super_user;
175+
if (!isSuperuser) {
176+
return res.boom.forbidden(UNAUTHORIZED_TO_UPDATE_REQUEST);
177+
}
178+
179+
const requestBody = req.body;
180+
const superUserId = req.userData.id;
181+
const requestId = req.params.id;
182+
183+
if (!requestId) {
184+
return res.boom.badRequest(REQUEST_ID_REQUIRED);
185+
}
186+
187+
const response = await oooRequestService.acknowledgeOooRequest(requestId, requestBody, superUserId);
188+
189+
return res.status(200).json({
190+
message: response.message,
191+
});
192+
}
193+
catch(error){
194+
logger.error(ERROR_WHILE_ACKNOWLEDGING_REQUEST, error);
195+
next(error);
196+
return res;
197+
}
198+
};

controllers/requests.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import {
55
} from "../constants/requests";
66
import { getRequests } from "../models/requests";
77
import { getPaginatedLink } from "../utils/helper";
8-
import { createOooRequestController, updateOooRequestController } from "./oooRequests";
9-
import { OooRequestCreateRequest, OooRequestResponse } from "../types/oooRequest";
8+
import { acknowledgeOooRequest, createOooRequestController, updateOooRequestController } from "./oooRequests";
9+
import { AcknowledgeOooRequest, OooRequestCreateRequest, OooRequestResponse } from "../types/oooRequest";
1010
import { CustomResponse } from "../typeDefinitions/global";
1111
import { ExtensionRequestRequest, ExtensionRequestResponse } from "../types/extensionRequests";
1212
import { createTaskExtensionRequest, updateTaskExtensionRequest } from "./extensionRequestsv2";
@@ -16,8 +16,7 @@ import { createTaskRequestController } from "./taskRequestsv2";
1616
import { OnboardingExtensionCreateRequest, OnboardingExtensionResponse, UpdateOnboardingExtensionStateRequest } from "../types/onboardingExtension";
1717
import { createOnboardingExtensionRequestController, updateOnboardingExtensionRequestController, updateOnboardingExtensionRequestState } from "./onboardingExtension";
1818
import { UpdateOnboardingExtensionRequest } from "../types/onboardingExtension";
19-
20-
import { Request } from "express";
19+
import { NextFunction, Request } from "express";
2120

2221
export const createRequestController = async (
2322
req: OooRequestCreateRequest | ExtensionRequestRequest | TaskRequestRequest | OnboardingExtensionCreateRequest,
@@ -121,9 +120,13 @@ export const getRequestsController = async (req: any, res: any) => {
121120
* @param {CustomResponse} res - The response object.
122121
* @returns {Promise<void>} Resolves or sends an error for invalid types.
123122
*/
124-
export const updateRequestBeforeAcknowledgedController = async (req: Request, res: CustomResponse) => {
123+
export const updateRequestBeforeAcknowledgedController = async (req: Request, res: CustomResponse, next: NextFunction) => {
125124
const type = req.body.type;
125+
126126
switch(type){
127+
case REQUEST_TYPE.OOO:
128+
await acknowledgeOooRequest(req as AcknowledgeOooRequest, res as OooRequestResponse, next);
129+
break;
127130
case REQUEST_TYPE.ONBOARDING:
128131
await updateOnboardingExtensionRequestController(req as UpdateOnboardingExtensionRequest, res as OnboardingExtensionResponse);
129132
break;

middlewares/validators/oooRequests.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import joi from "joi";
22
import { NextFunction } from "express";
33
import { REQUEST_STATE, REQUEST_TYPE } from "../../constants/requests";
4-
import { OooRequestCreateRequest, OooRequestResponse } from "../../types/oooRequest";
4+
import { AcknowledgeOooRequest, OooRequestCreateRequest, OooRequestResponse } from "../../types/oooRequest";
55

66
export const createOooStatusRequestValidator = async (
77
req: OooRequestCreateRequest,
@@ -38,3 +38,46 @@ export const createOooStatusRequestValidator = async (
3838

3939
await schema.validateAsync(req.body, { abortEarly: false });
4040
};
41+
42+
const schema = joi
43+
.object()
44+
.strict()
45+
.keys({
46+
comment: joi.string().optional()
47+
.messages({
48+
"string.empty": "comment cannot be empty",
49+
}),
50+
status: joi
51+
.string()
52+
.valid(REQUEST_STATE.APPROVED, REQUEST_STATE.REJECTED)
53+
.required()
54+
.messages({
55+
"any.only": "status must be APPROVED or REJECTED",
56+
}),
57+
type: joi.string().equal(REQUEST_TYPE.OOO).required().messages({
58+
"type.any": "type is required",
59+
})
60+
});
61+
62+
/**
63+
* Middleware to validate the acknowledge Out-Of-Office (OOO) request payload.
64+
*
65+
* @param {AcknowledgeOooRequest} req - The request object containing the body to be validated.
66+
* @param {OooRequestResponse} res - The response object used to send error responses if validation fails.
67+
* @param {NextFunction} next - The next middleware function to call if validation succeeds.
68+
* @returns {Promise<void>} Resolves or sends errors.
69+
*/
70+
export const acknowledgeOooRequest = async (
71+
req: AcknowledgeOooRequest,
72+
res: OooRequestResponse,
73+
next: NextFunction
74+
): Promise<void> => {
75+
try {
76+
await schema.validateAsync(req.body, { abortEarly: false });
77+
next();
78+
} catch (error) {
79+
const errorMessages = error.details.map((detail:{message: string}) => detail.message);
80+
logger.error(`Error while validating request payload : ${errorMessages}`);
81+
return res.boom.badRequest(errorMessages);
82+
}
83+
};

middlewares/validators/requests.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import joi from "joi";
22
import { NextFunction } from "express";
33
import { REQUEST_STATE, REQUEST_TYPE } from "../../constants/requests";
4-
import { OooRequestCreateRequest, OooRequestResponse } from "../../types/oooRequest";
5-
import { createOooStatusRequestValidator } from "./oooRequests";
4+
import { AcknowledgeOooRequest, OooRequestCreateRequest, OooRequestResponse } from "../../types/oooRequest";
5+
import { acknowledgeOooRequest, createOooStatusRequestValidator } from "./oooRequests";
66
import { createExtensionRequestValidator } from "./extensionRequestsv2";
77
import {createTaskRequestValidator} from "./taskRequests";
88
import { ExtensionRequestRequest, ExtensionRequestResponse } from "../../types/extensionRequests";
@@ -125,18 +125,24 @@ export const getRequestsMiddleware = async (req: OooRequestCreateRequest, res: O
125125
/**
126126
* Validates update requests based on their type.
127127
*
128-
* @param {UpdateOnboardingExtensionRequest} req - Request object.
128+
* @param {UpdateOnboardingExtensionRequest | AcknowledgeOooRequest} req - Request object.
129129
* @param {CustomResponse} res - Response object.
130130
* @param {NextFunction} next - Next middleware if valid.
131131
* @returns {Promise<void>} Resolves or sends errors.
132132
*/
133133
export const updateRequestValidator = async (
134-
req: UpdateOnboardingExtensionRequest,
134+
req: UpdateOnboardingExtensionRequest | AcknowledgeOooRequest,
135135
res: CustomResponse,
136136
next: NextFunction
137137
): Promise<void> => {
138138
const type = req.body.type;
139+
139140
switch (type) {
141+
case REQUEST_TYPE.OOO:
142+
await acknowledgeOooRequest(
143+
req,
144+
res as OooRequestResponse, next);
145+
break;
140146
case REQUEST_TYPE.ONBOARDING:
141147
await updateOnboardingExtensionRequestValidator(
142148
req,
@@ -145,4 +151,4 @@ export const updateRequestValidator = async (
145151
default:
146152
return res.boom.badRequest("Invalid type");
147153
}
148-
};
154+
};

models/requests.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
REQUEST_DOES_NOT_EXIST,
99
} from "../constants/requests";
1010
import { getUserId } from "../utils/users";
11+
import { NotFound } from "http-errors";
1112
const SIZE = 5;
1213

1314
export const createRequest = async (body: any) => {
@@ -69,6 +70,21 @@ export const updateRequest = async (id: string, body: any, lastModifiedBy: strin
6970
}
7071
};
7172

73+
export const getRequestById = async (id: string) => {
74+
try {
75+
const requestDoc = await requestModel.doc(id).get();
76+
77+
if (!requestDoc.exists) {
78+
throw new NotFound(REQUEST_DOES_NOT_EXIST);
79+
}
80+
81+
return requestDoc.data();
82+
} catch (error) {
83+
logger.error(ERROR_WHILE_FETCHING_REQUEST, error);
84+
throw error;
85+
}
86+
};
87+
7288
export const getRequests = async (query: any) => {
7389
let { id, type, requestedBy, state, prev, next, page, size = SIZE } = query;
7490
const dev = query.dev === "true";

services/oooRequest.ts

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,22 @@ import {
55
REQUEST_LOG_TYPE,
66
REQUEST_STATE,
77
USER_STATUS_NOT_FOUND,
8+
REQUEST_TYPE,
9+
REQUEST_ALREADY_APPROVED,
10+
REQUEST_ALREADY_REJECTED,
11+
REQUEST_APPROVED_SUCCESSFULLY,
12+
REQUEST_REJECTED_SUCCESSFULLY,
13+
INVALID_REQUEST_TYPE,
814
} from "../constants/requests";
915
import { userState } from "../constants/userStatus";
10-
import { createRequest } from "../models/requests";
16+
import { createRequest, getRequestById } from "../models/requests";
1117
import { OooStatusRequest, OooStatusRequestBody } from "../types/oooRequest";
1218
import { UserStatus } from "../types/userStatus";
1319
import { addLog } from "./logService";
20+
import { BadRequest, Conflict } from "http-errors";
21+
import { updateRequest } from "../models/requests";
22+
import { AcknowledgeOooRequestBody } from "../types/oooRequest";
23+
import { addFutureStatus } from "../models/userStatus";
1424

1525
/**
1626
* Validates the user status.
@@ -93,3 +103,101 @@ export const createOooRequest = async (
93103
throw error;
94104
}
95105
}
106+
107+
/**
108+
* Validates an Out-Of-Office (OOO) acknowledge request
109+
*
110+
* @param {string} requestId - The unique identifier of the request.
111+
* @param {string} requestType - The type of the request (expected to be 'OOO').
112+
* @param {string} requestStatus - The current status of the request.
113+
* @throws {Error} Throws an error if an issue occurs during validation.
114+
*/
115+
export const validateOooAcknowledgeRequest = async (
116+
requestType: string,
117+
requestStatus: string,
118+
) => {
119+
120+
try {
121+
122+
if (requestType !== REQUEST_TYPE.OOO) {
123+
throw new BadRequest(INVALID_REQUEST_TYPE);
124+
}
125+
126+
if (requestStatus === REQUEST_STATE.APPROVED) {
127+
throw new Conflict(REQUEST_ALREADY_APPROVED);
128+
}
129+
130+
if (requestStatus === REQUEST_STATE.REJECTED) {
131+
throw new Conflict(REQUEST_ALREADY_REJECTED);
132+
}
133+
} catch (error) {
134+
logger.error("Error while validating OOO acknowledge request", error);
135+
throw error;
136+
}
137+
}
138+
139+
/**
140+
* Acknowledges an Out-of-Office (OOO) request
141+
*
142+
* @param {string} requestId - The ID of the OOO request to acknowledge.
143+
* @param {AcknowledgeOooRequestBody} body - The acknowledgement body containing acknowledging details.
144+
* @param {string} superUserId - The unique identifier of the superuser user.
145+
* @returns {Promise<object>} The acknowledged OOO request.
146+
* @throws {Error} Throws an error if an issue occurs during acknowledgment process.
147+
*/
148+
export const acknowledgeOooRequest = async (
149+
requestId: string,
150+
body: AcknowledgeOooRequestBody,
151+
superUserId: string,
152+
) => {
153+
try {
154+
const requestData = await getRequestById(requestId);
155+
156+
await validateOooAcknowledgeRequest(requestData.type, requestData.status);
157+
158+
const requestResult = await updateRequest(requestId, body, superUserId, REQUEST_TYPE.OOO);
159+
160+
if ("error" in requestResult) {
161+
throw new BadRequest(requestResult.error);
162+
}
163+
164+
const [acknowledgeLogType, returnMessage] =
165+
requestResult.status === REQUEST_STATE.APPROVED
166+
? [REQUEST_LOG_TYPE.REQUEST_APPROVED, REQUEST_APPROVED_SUCCESSFULLY]
167+
: [REQUEST_LOG_TYPE.REQUEST_REJECTED, REQUEST_REJECTED_SUCCESSFULLY];
168+
169+
const requestLog = {
170+
type: acknowledgeLogType,
171+
meta: {
172+
requestId,
173+
action: LOG_ACTION.UPDATE,
174+
userId: superUserId,
175+
},
176+
body: requestResult,
177+
};
178+
179+
await addLog(requestLog.type, requestLog.meta, requestLog.body);
180+
181+
if (requestResult.status === REQUEST_STATE.APPROVED) {
182+
await addFutureStatus({
183+
requestId,
184+
state: REQUEST_TYPE.OOO,
185+
from: requestData.from,
186+
endsOn: requestData.until,
187+
userId: requestData.userId,
188+
message: body.comment,
189+
});
190+
}
191+
192+
return {
193+
message: returnMessage,
194+
data: {
195+
id: requestResult.id,
196+
...requestResult,
197+
},
198+
};
199+
} catch (error) {
200+
logger.error("Error while acknowledging OOO request", error);
201+
throw error;
202+
}
203+
}

test/fixtures/oooRequest/oooRequest.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ export const createOooRequests3 = {
168168
status: REQUEST_STATE.PENDING
169169
};
170170

171-
export const acknowledgeOooRequest = {
171+
export const testAcknowledgeOooRequest = {
172172
type: REQUEST_TYPE.OOO,
173173
status: REQUEST_STATE.APPROVED,
174174
comment: "OOO request approved as it's emergency."

0 commit comments

Comments
 (0)