From 36e067df2bd0346cb24c5d72db6b688cf1db91a0 Mon Sep 17 00:00:00 2001 From: VinuB-Dev Date: Tue, 26 Nov 2024 00:45:05 +0530 Subject: [PATCH] feat: Departed users api changes. --- controllers/users.js | 25 +++++++++++++ middlewares/validators/user.js | 1 + models/tasks.js | 21 +++++++++++ models/users.js | 65 +++++++++++++++++++++++++++++++--- services/users.js | 52 ++++++++------------------- 5 files changed, 122 insertions(+), 42 deletions(-) diff --git a/controllers/users.js b/controllers/users.js index 1fa81c282..c6cb0375e 100644 --- a/controllers/users.js +++ b/controllers/users.js @@ -30,6 +30,7 @@ const { addLog } = require("../models/logs"); const { getUserStatus } = require("../models/userStatus"); const config = require("config"); const { generateUniqueUsername } = require("../services/users"); +const userService = require("../services/users"); const discordDeveloperRoleId = config.get("discordDeveloperRoleId"); const verifyUser = async (req, res) => { @@ -191,6 +192,30 @@ const getUsers = async (req, res) => { } } + const isDeparted = req.query.departed === "true"; + + if (isDeparted) { + if (!dev) { + return res.boom.notFound("Route not found"); + } + try { + const result = await dataAccess.retrieveUsers({ query: req.query }); + const departedUsers = await userService.getUsersWithIncompleteTasks(result.users); + if (departedUsers.length === 0) return res.status(204).send(); + return res.json({ + message: "Users with abandoned tasks fetched successfully", + users: departedUsers, + links: { + next: result.nextId ? getPaginationLink(req.query, "next", result.nextId) : "", + prev: result.prevId ? getPaginationLink(req.query, "prev", result.prevId) : "", + }, + }); + } catch (error) { + logger.error("Error when fetching users who abandoned tasks:", error); + return res.boom.badImplementation(INTERNAL_SERVER_ERROR); + } + } + if (transformedQuery?.filterBy === OVERDUE_TASKS) { try { const tasksData = await getOverdueTasks(days); diff --git a/middlewares/validators/user.js b/middlewares/validators/user.js index 7d21a5161..2604ff51d 100644 --- a/middlewares/validators/user.js +++ b/middlewares/validators/user.js @@ -200,6 +200,7 @@ async function getUsers(req, res, next) { filterBy: joi.string().optional(), days: joi.string().optional(), dev: joi.string().optional(), + departed: joi.string().optional(), roles: joi.optional().custom((value, helpers) => { if (value !== "member") { return helpers.message("only member role is supported"); diff --git a/models/tasks.js b/models/tasks.js index 8b0754b1b..c9fa3885c 100644 --- a/models/tasks.js +++ b/models/tasks.js @@ -701,6 +701,26 @@ const markUnDoneTasksOfArchivedUsersBacklog = async (users) => { } }; +/** + * Fetch incomplete tasks assigned to a specific user + * @param {string} userId - The unique identifier for the user. + * @returns {Promise} - A promise that resolves to an array of incomplete tasks for the given user. + * @throws {Error} - Throws an error if the database query fails. + */ +const fetchIncompleteTaskForUser = async (userId) => { + const COMPLETED_STATUSES = [DONE, COMPLETED]; + try { + const incompleteTaskForUser = await tasksModel + .where("assigneeId", "==", userId) + .where("status", "not-in", COMPLETED_STATUSES) + .get(); + return incompleteTaskForUser; + } catch (error) { + logger.error("Error when fetching incomplete tasks:", error); + throw error; + } +}; + module.exports = { updateTask, fetchTasks, @@ -720,4 +740,5 @@ module.exports = { updateTaskStatus, updateOrphanTasksStatus, markUnDoneTasksOfArchivedUsersBacklog, + fetchIncompleteTaskForUser, }; diff --git a/models/users.js b/models/users.js index e0746de92..88a65a704 100644 --- a/models/users.js +++ b/models/users.js @@ -8,8 +8,12 @@ const firestore = require("../utils/firestore"); const { fetchWallet, createWallet } = require("../models/wallets"); const { updateUserStatus } = require("../models/userStatus"); const { arraysHaveCommonItem, chunks } = require("../utils/array"); -const { archiveUsers } = require("../services/users"); -const { ALLOWED_FILTER_PARAMS, FIRESTORE_IN_CLAUSE_SIZE } = require("../constants/users"); +const { + ALLOWED_FILTER_PARAMS, + FIRESTORE_IN_CLAUSE_SIZE, + USERS_PATCH_HANDLER_SUCCESS_MESSAGES, + USERS_PATCH_HANDLER_ERROR_MESSAGES, +} = require("../constants/users"); const { DOCUMENT_WRITE_SIZE } = require("../constants/constants"); const { userState } = require("../constants/userStatus"); const { BATCH_SIZE_IN_CLAUSE } = require("../constants/firebase"); @@ -27,6 +31,52 @@ const { formatUsername } = require("../utils/username"); const { logType } = require("../constants/logs"); const { addLog } = require("../services/logService"); +/** + * Archive users by setting the roles.archived field to true. + * This function commits the write in batches to avoid reaching the maximum number of writes per batch. + * @param {Array} usersData - An array of user objects with the following properties: id, first_name, last_name + * @returns {Promise} - A promise that resolves with a summary object containing the number of users updated and failed, and an array of updated and failed user details. + */ +const archiveUsers = async (usersData) => { + const batch = firestore.batch(); + const usersBatch = []; + const summary = { + totalUsersArchived: 0, + totalOperationsFailed: 0, + updatedUserDetails: [], + failedUserDetails: [], + }; + + usersData.forEach((user) => { + const { id, first_name: firstName, last_name: lastName } = user; + const updatedUserData = { + ...user, + roles: { + ...user.roles, + archived: true, + }, + updated_at: Date.now(), + }; + batch.update(userModel.doc(id), updatedUserData); + usersBatch.push({ id, firstName, lastName }); + }); + + try { + await batch.commit(); + summary.totalUsersArchived += usersData.length; + summary.updatedUserDetails = [...usersBatch]; + return { + message: USERS_PATCH_HANDLER_SUCCESS_MESSAGES.ARCHIVE_USERS.SUCCESSFULLY_COMPLETED_BATCH_UPDATES, + ...summary, + }; + } catch (err) { + logger.error("Firebase batch Operation Failed!"); + summary.totalOperationsFailed += usersData.length; + summary.failedUserDetails = [...usersBatch]; + return { message: USERS_PATCH_HANDLER_ERROR_MESSAGES.ARCHIVE_USERS.BATCH_DATA_UPDATED_FAILED, ...summary }; + } +}; + /** * Adds or updates the user data * @@ -182,11 +232,11 @@ const getSuggestedUsers = async (skill) => { */ const fetchPaginatedUsers = async (query) => { const isDevMode = query.dev === "true"; - try { const size = parseInt(query.size) || 100; const doc = (query.next || query.prev) && (await userModel.doc(query.next || query.prev).get()); + const isArchived = query.departed === "true"; let dbQuery; /** * !!NOTE : At the time of writing we only support member in the role query @@ -195,9 +245,9 @@ const fetchPaginatedUsers = async (query) => { * if you're making changes to this code remove the archived check in the role query, example: role=archived,member */ if (query.roles === "member") { - dbQuery = userModel.where("roles.archived", "==", false).where("roles.member", "==", true); + dbQuery = userModel.where("roles.archived", "==", isArchived).where("roles.member", "==", true); } else { - dbQuery = userModel.where("roles.archived", "==", false).orderBy("username"); + dbQuery = userModel.where("roles.archived", "==", isArchived).orderBy("username"); } let compositeQuery = [dbQuery]; @@ -217,6 +267,10 @@ const fetchPaginatedUsers = async (query) => { } if (Object.keys(query).length) { + if (query.departed) { + compositeQuery = compositeQuery.map((query) => query.where("roles.in_discord", "==", false)); + dbQuery = dbQuery.where("roles.in_discord", "==", false); + } if (query.search) { const searchValue = query.search.toLowerCase().trim(); dbQuery = dbQuery.startAt(searchValue).endAt(searchValue + "\uf8ff"); @@ -1031,6 +1085,7 @@ const updateUsersWithNewUsernames = async () => { }; module.exports = { + archiveUsers, addOrUpdate, fetchPaginatedUsers, fetchUser, diff --git a/services/users.js b/services/users.js index bb1d609b6..cf8fa4156 100644 --- a/services/users.js +++ b/services/users.js @@ -1,44 +1,22 @@ -const { USERS_PATCH_HANDLER_SUCCESS_MESSAGES, USERS_PATCH_HANDLER_ERROR_MESSAGES } = require("../constants/users"); const firestore = require("../utils/firestore"); const { formatUsername } = require("../utils/username"); const userModel = firestore.collection("users"); -const archiveUsers = async (usersData) => { - const batch = firestore.batch(); - const usersBatch = []; - const summary = { - totalUsersArchived: 0, - totalOperationsFailed: 0, - updatedUserDetails: [], - failedUserDetails: [], - }; - - usersData.forEach((user) => { - const { id, first_name: firstName, last_name: lastName } = user; - const updatedUserData = { - ...user, - roles: { - ...user.roles, - archived: true, - }, - updated_at: Date.now(), - }; - batch.update(userModel.doc(id), updatedUserData); - usersBatch.push({ id, firstName, lastName }); - }); +const tasksQuery = require("../models/tasks"); +const getUsersWithIncompleteTasks = async (users) => { + if (users.length === 0) return []; try { - await batch.commit(); - summary.totalUsersArchived += usersData.length; - summary.updatedUserDetails = [...usersBatch]; - return { - message: USERS_PATCH_HANDLER_SUCCESS_MESSAGES.ARCHIVE_USERS.SUCCESSFULLY_COMPLETED_BATCH_UPDATES, - ...summary, - }; - } catch (err) { - logger.error("Firebase batch Operation Failed!"); - summary.totalOperationsFailed += usersData.length; - summary.failedUserDetails = [...usersBatch]; - return { message: USERS_PATCH_HANDLER_ERROR_MESSAGES.ARCHIVE_USERS.BATCH_DATA_UPDATED_FAILED, ...summary }; + const eligibleUsersWithTasks = []; + for (const user of users) { + const abandonedTasksQuerySnapshot = await tasksQuery.fetchIncompleteTaskForUser(user.id); + if (!abandonedTasksQuerySnapshot.empty) { + eligibleUsersWithTasks.push(user); + } + } + return eligibleUsersWithTasks; + } catch (error) { + logger.error(`Error in getting users who abandoned tasks: ${error}`); + throw error; } }; @@ -63,6 +41,6 @@ const generateUniqueUsername = async (firstName, lastName) => { }; module.exports = { - archiveUsers, generateUniqueUsername, + getUsersWithIncompleteTasks, };