Skip to content

Commit

Permalink
feat: Departed users api changes.
Browse files Browse the repository at this point in the history
  • Loading branch information
VinuB-Dev committed Nov 25, 2024
1 parent b6351b7 commit 36e067d
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 42 deletions.
25 changes: 25 additions & 0 deletions controllers/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions middlewares/validators/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
21 changes: 21 additions & 0 deletions models/tasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<Array>} - 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,
Expand All @@ -720,4 +740,5 @@ module.exports = {
updateTaskStatus,
updateOrphanTasksStatus,
markUnDoneTasksOfArchivedUsersBacklog,
fetchIncompleteTaskForUser,
};
65 changes: 60 additions & 5 deletions models/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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
*
Expand Down Expand Up @@ -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
Expand All @@ -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];
Expand All @@ -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");
Expand Down Expand Up @@ -1031,6 +1085,7 @@ const updateUsersWithNewUsernames = async () => {
};

module.exports = {
archiveUsers,
addOrUpdate,
fetchPaginatedUsers,
fetchUser,
Expand Down
52 changes: 15 additions & 37 deletions services/users.js
Original file line number Diff line number Diff line change
@@ -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;
}
};

Expand All @@ -63,6 +41,6 @@ const generateUniqueUsername = async (firstName, lastName) => {
};

module.exports = {
archiveUsers,
generateUniqueUsername,
getUsersWithIncompleteTasks,
};

0 comments on commit 36e067d

Please sign in to comment.