Skip to content

Commit 4bf146e

Browse files
Achintya-ChatterjeepankajjsAnujChhikaraprakashchoudhary07vikasosmium
authored
Dev to Main Sync (#2336)
* feat: Add api to create onboarding extension request from discord server (#2307) * fix:added onboarding type in request * feat: added types for onboarding extension request * feat: added validator and skip-authenticate middleware * fix: added missing field in user type * feat: added controller for handling the create onboarding extension request feature * fix: remove requestedBy field because validation for super-users is done before hitting this api * refactor: moved constant messages from controller * fix: remove super-users validation check as it is done before making this api call * fix: create newEndsOn from current date when deadline has missed * fix: wrap schema validation logic in try-catch block * chore: refactor varibale name for better readability * chore: refactor new deadline calculation logic in a separate utils file for reuse * chore: use utils function to calculate days to milliseconds * feat: added utils function to validate date * fix: return error response for invalid date * fix: return forbidden response for non-onboarding user * chore: added semicolon for consistent code practise * chore: added jsDoc for functions and refactor import statment * fix: import addLog from service file and fix lint issue * Add tests for create onboarding extension request api (#2306) * feat: added tests for middlewares * feat:added tests for controller * refactor: create middleware in beforeEach hook * refactor: remove requestBy field and super-users validation tests * fix: expectation message as test was failing * fix: change order of middleware creation as test was failing * fix: assert statement and mock value * feat: added tests for handling edges cases and fixed existing test * chore: correct test name * chore: fix lint issue * feat: Integrate userData into Progresses API to reduce redundant calls (#2311) * initial * fix typos * using batches to fetch userIds * refactor the function * added test for dev false case * added unit tests * fix response body containing email --------- Co-authored-by: Achintya Chatterjee <[email protected]> Co-authored-by: Prakash Choudhary <[email protected]> * fix: Changed status code to 409 for sending Conflict response (#2339) * chore(#2342) - upgraded dependency packages * Revert "chore(#2342) - upgraded dependency packages" (#2343) This reverts commit 5bc49c6. --------- Co-authored-by: Pankaj <[email protected]> Co-authored-by: Anuj Chhikara <[email protected]> Co-authored-by: Prakash Choudhary <[email protected]> Co-authored-by: Vikas Singh <[email protected]> Co-authored-by: Yash Raj <[email protected]>
1 parent 17c9605 commit 4bf146e

20 files changed

+1039
-10
lines changed

constants/requests.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const REQUEST_TYPE = {
1515
EXTENSION: "EXTENSION",
1616
TASK: "TASK",
1717
ALL: "ALL",
18+
ONBOARDING: "ONBOARDING",
1819
};
1920

2021
export const REQUEST_LOG_TYPE = {
@@ -53,3 +54,6 @@ export const TASK_REQUEST_MESSAGES = {
5354
ERROR_CREATING_TASK_REQUEST: "Error while creating task request",
5455
TASK_REQUEST_UPDATED_SUCCESS: "Task request updated successfully",
5556
};
57+
58+
export const ONBOARDING_REQUEST_CREATED_SUCCESSFULLY = "Onboarding extension request created successfully"
59+
export const UNAUTHORIZED_TO_CREATE_ONBOARDING_EXTENSION_REQUEST = "Only super user and onboarding user are authorized to create an onboarding extension request"

controllers/onboardingExtension.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import {
2+
ERROR_WHILE_CREATING_REQUEST,
3+
LOG_ACTION,
4+
ONBOARDING_REQUEST_CREATED_SUCCESSFULLY,
5+
REQUEST_ALREADY_PENDING,
6+
REQUEST_LOG_TYPE,
7+
REQUEST_STATE,
8+
REQUEST_TYPE,
9+
UNAUTHORIZED_TO_CREATE_ONBOARDING_EXTENSION_REQUEST,
10+
} from "../constants/requests";
11+
import { userState } from "../constants/userStatus";
12+
import { addLog } from "../services/logService";
13+
import { createRequest, getRequestByKeyValues } from "../models/requests";
14+
import { fetchUser } from "../models/users";
15+
import { getUserStatus } from "../models/userStatus";
16+
import { User } from "../typeDefinitions/users";
17+
import {
18+
CreateOnboardingExtensionBody,
19+
OnboardingExtension,
20+
OnboardingExtensionCreateRequest,
21+
OnboardingExtensionResponse
22+
} from "../types/onboardingExtension";
23+
import { convertDateStringToMilliseconds, getNewDeadline } from "../utils/requests";
24+
import { convertDaysToMilliseconds } from "../utils/time";
25+
26+
/**
27+
* Controller to handle the creation of onboarding extension requests.
28+
*
29+
* This function processes the request to create an extension for the onboarding period,
30+
* validates the user status, checks existing requests, calculates new deadlines,
31+
* and stores the new request in the database with logging.
32+
*
33+
* @param {OnboardingExtensionCreateRequest} req - The Express request object containing the body with extension details.
34+
* @param {OnboardingExtensionResponse} res - The Express response object used to send back the response.
35+
* @returns {Promise<OnboardingExtensionResponse>} Resolves to a response with the status and data or an error message.
36+
*/
37+
export const createOnboardingExtensionRequestController = async (req: OnboardingExtensionCreateRequest, res: OnboardingExtensionResponse): Promise<OnboardingExtensionResponse> => {
38+
try {
39+
40+
const data = req.body as CreateOnboardingExtensionBody;
41+
const {user, userExists} = await fetchUser({discordId: data.userId});
42+
43+
if(!userExists) {
44+
return res.boom.notFound("User not found");
45+
}
46+
47+
const { id: userId, discordJoinedAt, username} = user as User;
48+
const { data: userStatus } = await getUserStatus(userId);
49+
50+
if(!userStatus || userStatus.currentStatus.state != userState.ONBOARDING){
51+
return res.boom.forbidden(UNAUTHORIZED_TO_CREATE_ONBOARDING_EXTENSION_REQUEST);
52+
}
53+
54+
const latestExtensionRequest: OnboardingExtension = await getRequestByKeyValues({
55+
userId: userId,
56+
type: REQUEST_TYPE.ONBOARDING
57+
});
58+
59+
if(latestExtensionRequest && latestExtensionRequest.state === REQUEST_STATE.PENDING){
60+
return res.boom.conflict(REQUEST_ALREADY_PENDING);
61+
}
62+
63+
const millisecondsInThirtyOneDays = convertDaysToMilliseconds(31);
64+
const numberOfDaysInMillisecond = convertDaysToMilliseconds(data.numberOfDays);
65+
const { isDate, milliseconds: discordJoinedDateInMillisecond } = convertDateStringToMilliseconds(discordJoinedAt);
66+
67+
if(!isDate){
68+
logger.error(ERROR_WHILE_CREATING_REQUEST, "Invalid date");
69+
return res.boom.badImplementation(ERROR_WHILE_CREATING_REQUEST);
70+
}
71+
72+
let requestNumber: number;
73+
let oldEndsOn: number;
74+
const currentDate = Date.now();
75+
76+
if(!latestExtensionRequest){
77+
requestNumber = 1;
78+
oldEndsOn = discordJoinedDateInMillisecond + millisecondsInThirtyOneDays;
79+
}else if(latestExtensionRequest.state === REQUEST_STATE.REJECTED) {
80+
requestNumber = latestExtensionRequest.requestNumber + 1;
81+
oldEndsOn = latestExtensionRequest.oldEndsOn;
82+
}else{
83+
requestNumber = latestExtensionRequest.requestNumber + 1;
84+
oldEndsOn = latestExtensionRequest.newEndsOn;
85+
}
86+
87+
const newEndsOn = getNewDeadline(currentDate, oldEndsOn, numberOfDaysInMillisecond);
88+
89+
const onboardingExtension = await createRequest({
90+
type: REQUEST_TYPE.ONBOARDING,
91+
state: REQUEST_STATE.PENDING,
92+
userId: userId,
93+
requestedBy: username,
94+
oldEndsOn: oldEndsOn,
95+
newEndsOn: newEndsOn,
96+
reason: data.reason,
97+
requestNumber: requestNumber,
98+
});
99+
100+
const onboardingExtensionLog = {
101+
type: REQUEST_LOG_TYPE.REQUEST_CREATED,
102+
meta: {
103+
requestId: onboardingExtension.id,
104+
action: LOG_ACTION.CREATE,
105+
userId: userId,
106+
createdAt: Date.now(),
107+
},
108+
body: onboardingExtension,
109+
};
110+
111+
await addLog(onboardingExtensionLog.type, onboardingExtensionLog.meta, onboardingExtensionLog.body);
112+
113+
return res.status(201).json({
114+
message: ONBOARDING_REQUEST_CREATED_SUCCESSFULLY,
115+
data: {
116+
id: onboardingExtension.id,
117+
...onboardingExtension,
118+
}
119+
});
120+
}catch (err) {
121+
logger.error(ERROR_WHILE_CREATING_REQUEST, err);
122+
return res.boom.badImplementation(ERROR_WHILE_CREATING_REQUEST);
123+
}
124+
};

controllers/progresses.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ const getProgressRangeData = async (req, res) => {
217217

218218
const getProgressBydDateController = async (req, res) => {
219219
try {
220-
const data = await getProgressByDate(req.params);
220+
const data = await getProgressByDate(req.params, req.query);
221221
return res.json({
222222
message: PROGRESS_DOCUMENT_RETRIEVAL_SUCCEEDED,
223223
data,

controllers/requests.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ import { createTaskExtensionRequest, updateTaskExtensionRequest } from "./extens
1313
import { UpdateRequest } from "../types/requests";
1414
import { TaskRequestRequest } from "../types/taskRequests";
1515
import { createTaskRequestController } from "./taskRequestsv2";
16+
import { OnboardingExtensionCreateRequest, OnboardingExtensionResponse } from "../types/onboardingExtension";
17+
import { createOnboardingExtensionRequestController } from "./onboardingExtension";
1618

1719
export const createRequestController = async (
18-
req: OooRequestCreateRequest | ExtensionRequestRequest | TaskRequestRequest,
20+
req: OooRequestCreateRequest | ExtensionRequestRequest | TaskRequestRequest | OnboardingExtensionCreateRequest,
1921
res: CustomResponse
2022
) => {
2123
const type = req.body.type;
@@ -26,6 +28,8 @@ export const createRequestController = async (
2628
return await createTaskExtensionRequest(req as ExtensionRequestRequest, res as ExtensionRequestResponse);
2729
case REQUEST_TYPE.TASK:
2830
return await createTaskRequestController(req as TaskRequestRequest, res as CustomResponse);
31+
case REQUEST_TYPE.ONBOARDING:
32+
return await createOnboardingExtensionRequestController(req as OnboardingExtensionCreateRequest, res as OnboardingExtensionResponse);
2933
default:
3034
return res.boom.badRequest("Invalid request type");
3135
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { NextFunction, Request, Response } from "express"
2+
import { REQUEST_TYPE } from "../constants/requests";
3+
/**
4+
* Middleware to selectively authenticate or verify Discord bot based on the request type.
5+
* Specifically handles requests for onboarding extensions by skipping authentication.
6+
*
7+
* @param {Function} authenticate - The authentication middleware to apply for general requests.
8+
* @param {Function} verifyDiscordBot - The middleware to verify requests from a Discord bot.
9+
* @returns {Function} A middleware function that processes the request based on its type.
10+
*
11+
* @example
12+
* app.use(skipAuthenticateForOnboardingExtensionRequest(authenticate, verifyDiscordBot));
13+
*/
14+
export const skipAuthenticateForOnboardingExtensionRequest = (authenticate, verifyDiscordBot) => {
15+
return async (req: Request, res: Response, next: NextFunction) => {
16+
const type = req.body.type;
17+
const dev = req.query.dev;
18+
19+
if(type === REQUEST_TYPE.ONBOARDING){
20+
if (dev != "true"){
21+
return res.status(501).json({
22+
message: "Feature not implemented"
23+
})
24+
}
25+
return await verifyDiscordBot(req, res, next);
26+
}
27+
28+
return await authenticate(req, res, next)
29+
}
30+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import joi from "joi";
2+
import { NextFunction } from "express";
3+
import { REQUEST_TYPE } from "../../constants/requests";
4+
import { OnboardingExtensionCreateRequest, OnboardingExtensionResponse } from "../../types/onboardingExtension";
5+
6+
export const createOnboardingExtensionRequestValidator = async (
7+
req: OnboardingExtensionCreateRequest,
8+
_res: OnboardingExtensionResponse,
9+
_next: NextFunction
10+
) => {
11+
12+
const schema = joi
13+
.object()
14+
.strict()
15+
.keys({
16+
numberOfDays: joi.number().required().positive().integer().min(1).messages({
17+
"number.base": "numberOfDays must be a number",
18+
"any.required": "numberOfDays is required",
19+
"number.positive": "numberOfDays must be positive",
20+
"number.min": "numberOfDays must be greater than zero",
21+
"number.integer": "numberOfDays must be a integer"
22+
}),
23+
reason: joi.string().required().messages({
24+
"string.empty": "reason cannot be empty",
25+
"any.required": "reason is required",
26+
}),
27+
type: joi.string().valid(REQUEST_TYPE.ONBOARDING).required().messages({
28+
"string.empty": "type cannot be empty",
29+
"any.required": "type is required",
30+
}),
31+
userId: joi.string().required().messages({
32+
"string.empty": "userId cannot be empty",
33+
"any.required": "userId is required"
34+
})
35+
});
36+
try{
37+
await schema.validateAsync(req.body, { abortEarly: false });
38+
}catch(error){
39+
logger.error(`Error while validating request payload`, error);
40+
throw error;
41+
}
42+
};

middlewares/validators/progresses.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ const validateGetProgressRecordsQuery = async (req, res, next) => {
6363
taskId: joi.string().optional().allow("").messages({
6464
"string.base": "taskId must be a string",
6565
}),
66+
dev: joi.boolean().optional().messages({
67+
"boolean.base": "dev must be a boolean value (true or false).",
68+
}),
6669
orderBy: joi
6770
.string()
6871
.optional()
@@ -92,6 +95,7 @@ const validateGetRangeProgressRecordsParams = async (req, res, next) => {
9295
taskId: joi.string().optional(),
9396
startDate: joi.date().iso().required(),
9497
endDate: joi.date().iso().min(joi.ref("startDate")).required(),
98+
dev: joi.boolean().optional(),
9599
})
96100
.xor("userId", "taskId")
97101
.messages({
@@ -121,6 +125,7 @@ const validateGetDayProgressParams = async (req, res, next) => {
121125
}),
122126
typeId: joi.string().required(),
123127
date: joi.date().iso().required(),
128+
dev: joi.boolean().optional(),
124129
});
125130
try {
126131
await schema.validateAsync(req.params, { abortEarly: false });

middlewares/validators/requests.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import { ExtensionRequestRequest, ExtensionRequestResponse } from "../../types/e
99
import { CustomResponse } from "../../typeDefinitions/global";
1010
import { UpdateRequest } from "../../types/requests";
1111
import { TaskRequestRequest, TaskRequestResponse } from "../../types/taskRequests";
12+
import { createOnboardingExtensionRequestValidator } from "./onboardingExtensionRequest";
13+
import { OnboardingExtensionCreateRequest, OnboardingExtensionResponse } from "../../types/onboardingExtension";
1214

1315
export const createRequestsMiddleware = async (
14-
req: OooRequestCreateRequest|ExtensionRequestRequest | TaskRequestRequest,
16+
req: OooRequestCreateRequest|ExtensionRequestRequest | TaskRequestRequest | OnboardingExtensionCreateRequest,
1517
res: CustomResponse,
1618
next: NextFunction
1719
) => {
@@ -28,6 +30,9 @@ export const createRequestsMiddleware = async (
2830
case REQUEST_TYPE.TASK:
2931
await createTaskRequestValidator(req as TaskRequestRequest, res as TaskRequestResponse, next);
3032
break;
33+
case REQUEST_TYPE.ONBOARDING:
34+
await createOnboardingExtensionRequestValidator(req as OnboardingExtensionCreateRequest, res as OnboardingExtensionResponse, next);
35+
break;
3136
default:
3237
res.boom.badRequest(`Invalid request type: ${type}`);
3338
}
@@ -36,7 +41,7 @@ export const createRequestsMiddleware = async (
3641
} catch (error) {
3742
const errorMessages = error.details.map((detail:any) => detail.message);
3843
logger.error(`Error while validating request payload : ${errorMessages}`);
39-
res.boom.badRequest(errorMessages);
44+
return res.boom.badRequest(errorMessages);
4045
}
4146
};
4247

models/progresses.js

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const {
1313
getProgressDateTimestamp,
1414
buildQueryToSearchProgressByDay,
1515
} = require("../utils/progresses");
16+
const { retrieveUsers } = require("../services/dataAccessLayer");
1617
const { PROGRESS_ALREADY_CREATED, PROGRESS_DOCUMENT_NOT_FOUND } = PROGRESSES_RESPONSE_MESSAGES;
1718

1819
/**
@@ -47,9 +48,14 @@ const createProgressDocument = async (progressData) => {
4748
* @throws {Error} If the userId or taskId is invalid or does not exist.
4849
**/
4950
const getProgressDocument = async (queryParams) => {
51+
const { dev } = queryParams;
5052
await assertUserOrTaskExists(queryParams);
5153
const query = buildQueryToFetchDocs(queryParams);
5254
const progressDocs = await getProgressDocs(query);
55+
56+
if (dev === "true") {
57+
return await addUserDetailsToProgressDocs(progressDocs);
58+
}
5359
return progressDocs;
5460
};
5561

@@ -77,16 +83,59 @@ const getRangeProgressData = async (queryParams) => {
7783
* @returns {Promise<object>} A Promise that resolves with the progress records of the queried user or task.
7884
* @throws {Error} If the userId or taskId is invalid or does not exist.
7985
**/
80-
async function getProgressByDate(pathParams) {
86+
async function getProgressByDate(pathParams, queryParams) {
8187
const { type, typeId, date } = pathParams;
88+
const { dev } = queryParams;
8289
await assertUserOrTaskExists({ [TYPE_MAP[type]]: typeId });
8390
const query = buildQueryToSearchProgressByDay({ [TYPE_MAP[type]]: typeId, date });
8491
const result = await query.get();
8592
if (!result.size) {
8693
throw new NotFound(PROGRESS_DOCUMENT_NOT_FOUND);
8794
}
8895
const doc = result.docs[0];
89-
return { id: doc.id, ...doc.data() };
96+
const docData = doc.data();
97+
if (dev === "true") {
98+
const { user: userData } = await retrieveUsers({ id: docData.userId });
99+
return { id: doc.id, ...docData, userData };
100+
}
101+
102+
return { id: doc.id, ...docData };
90103
}
91104

92-
module.exports = { createProgressDocument, getProgressDocument, getRangeProgressData, getProgressByDate };
105+
/**
106+
* Adds user details to progress documents by fetching unique users.
107+
* This function retrieves user details for each user ID in the progress documents and attaches the user data to each document.
108+
*
109+
* @param {Array<object>} progressDocs - An array of progress documents. Each document should include a `userId` property.
110+
* @returns {Promise<Array<object>>} A Promise that resolves to an array of progress documents with the `userData` field populated.
111+
* If an error occurs while fetching the user details, the `userData` field will be set to `null` for each document.
112+
*/
113+
const addUserDetailsToProgressDocs = async (progressDocs) => {
114+
try {
115+
const uniqueUserIds = [...new Set(progressDocs.map((doc) => doc.userId))];
116+
117+
const uniqueUsersData = await retrieveUsers({
118+
userIds: uniqueUserIds,
119+
});
120+
const allUsers = uniqueUsersData.flat();
121+
const userByIdMap = allUsers.reduce((lookup, user) => {
122+
if (user) lookup[user.id] = user;
123+
return lookup;
124+
}, {});
125+
126+
return progressDocs.map((doc) => {
127+
const userDetails = userByIdMap[doc.userId] || null;
128+
return { ...doc, userData: userDetails };
129+
});
130+
} catch (err) {
131+
return progressDocs.map((doc) => ({ ...doc, userData: null }));
132+
}
133+
};
134+
135+
module.exports = {
136+
createProgressDocument,
137+
getProgressDocument,
138+
getRangeProgressData,
139+
getProgressByDate,
140+
addUserDetailsToProgressDocs,
141+
};

0 commit comments

Comments
 (0)