Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 24 additions & 18 deletions models/discordactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const { findSubscribedGroupIds } = require("../utils/helper");
const { retrieveUsers } = require("../services/dataAccessLayer");
const { BATCH_SIZE_IN_CLAUSE } = require("../constants/firebase");
const { getAllUserStatus, getGroupRole, getUserStatus } = require("./userStatus");
const { normalizeTimestamp, checkIfUserHasLiveTasks } = require("../utils/userStatus");
const { normalizeTimestamp, checkIfUserHasLiveTasks, computeIdleDaysExcludingOOO } = require("../utils/userStatus");
const { userState, POST_OOO_GRACE_PERIOD_IN_DAYS } = require("../constants/userStatus");
const config = require("config");
const logger = require("../utils/logger");
Expand Down Expand Up @@ -680,26 +680,32 @@ const updateIdle7dUsersOnDiscord = async (dev) => {
});

if (allUserStatus) {
const nowMs = Date.now();
await Promise.all(
allUserStatus.map(async (userStatus) => {
const currentDate = new Date();
const lastDate = new Date(userStatus.currentStatus.from);
const ONE_DAY = 1000 * 60 * 60 * 24;
const timeDifference = currentDate.setUTCHours(0, 0, 0, 0) - lastDate.setUTCHours(0, 0, 0, 0);
const daysDifference = Math.floor(timeDifference / ONE_DAY);
try {
if (daysDifference > 7) {
const userData = await userModel.doc(userStatus.userId).get();
const isUserArchived = userData.data().roles.archived;
if (userData.exists) {
if (isUserArchived) {
totalArchivedUsers++;
} else if (dev === "true" && !allMavens.includes(userData.data().discordId)) {
const shouldAdd = await shouldAddIdleUser(userStatus, tasksModel);
if (shouldAdd) {
userStatus.userid = userData.data().discordId;
allIdle7dUsers.push(userStatus);
}
const fullStatusDoc = await userStatusModel.doc(userStatus.id).get();
const fullData = fullStatusDoc.exists ? fullStatusDoc.data() : {};
const idleDays = computeIdleDaysExcludingOOO(
fullData.idleWindowStartedAt,
fullData.lastOooFrom,
fullData.lastOooUntil,
userStatus.currentStatus?.from,
nowMs
);
if (idleDays <= 7) {
return;
}
const userData = await userModel.doc(userStatus.userId).get();
const isUserArchived = userData.data()?.roles?.archived;
if (userData.exists) {
if (isUserArchived) {
totalArchivedUsers++;
} else if (dev === "true" && !allMavens.includes(userData.data().discordId)) {
const shouldAdd = await shouldAddIdleUser(userStatus, tasksModel);
if (shouldAdd) {
userStatus.userid = userData.data().discordId;
allIdle7dUsers.push(userStatus);
}
}
}
Expand Down
33 changes: 29 additions & 4 deletions models/userStatus.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,12 @@ const updateUserStatus = async (userId, updatedStatusData) => {
});
if (lastOooUntilUpdate !== undefined) {
newStatusData.lastOooUntil = lastOooUntilUpdate;
if (previousState === userState.OOO && previousCurrentStatus.from != null) {
newStatusData.lastOooFrom = previousCurrentStatus.from;
}
}
if (requestedNextState === userState.IDLE && previousState === userState.ACTIVE) {
newStatusData.idleWindowStartedAt = newStatusData.currentStatus?.from ?? newStatusData.currentStatus?.updatedAt;
}
if (
userStatusData.currentStatus?.state === userState.IDLE &&
Expand All @@ -270,7 +276,11 @@ const updateUserStatus = async (userId, updatedStatusData) => {
}
}
}
const { id } = await userStatusModel.add({ userId, lastOooUntil: null, ...newStatusData });
const initialData = { userId, lastOooUntil: null, ...newStatusData };
if (newStatusData.currentStatus?.state === userState.IDLE) {
initialData.idleWindowStartedAt = newStatusData.currentStatus.from ?? newStatusData.currentStatus.updatedAt;
}
const { id } = await userStatusModel.add(initialData);
return { id, userStatusExists: false, data: newStatusData };
}
} catch (error) {
Expand Down Expand Up @@ -321,6 +331,9 @@ const updateAllUserStatus = async () => {
});
if (lastOooUntilUpdate !== undefined) {
newStatusData.lastOooUntil = lastOooUntilUpdate;
if (currentState === userState.OOO && currentStatus?.from != null) {
newStatusData.lastOooFrom = currentStatus.from;
}
}
toUpdate = !toUpdate;
summary.oooUsersAltered++;
Expand All @@ -345,6 +358,7 @@ const updateAllUserStatus = async () => {
newStatusData.currentStatus = newCurrentStatus;
newStatusData.futureStatus = newFutureStatus;
newStatusData.lastOooUntil = null;
newStatusData.lastOooFrom = null;
toUpdate = !toUpdate;
summary.nonOooUsersAltered++;
} else {
Expand Down Expand Up @@ -574,10 +588,15 @@ const batchUpdateUsersStatus = async (users) => {
nextState,
fallbackTimestamp: currentTimeStamp,
});
batch.update(docRef, {
const currentStatusData = data?.currentStatus || {};
const batchUpdateData = {
currentStatus: statusToUpdate,
...(lastOooUntilUpdate !== undefined && { lastOooUntil: lastOooUntilUpdate }),
});
...(lastOooUntilUpdate !== undefined && {
lastOooUntil: lastOooUntilUpdate,
...(currentStatusData.from != null && { lastOooFrom: currentStatusData.from }),
}),
};
batch.update(docRef, batchUpdateData);
} else {
const getNextDayAfterUntil = getNextDayTimeStamp(currentUntil);
batch.update(docRef, {
Expand All @@ -603,6 +622,9 @@ const batchUpdateUsersStatus = async (users) => {
if (lastOooUntilUpdate !== undefined) {
updatedStatusData.lastOooUntil = lastOooUntilUpdate;
}
if (state === userState.IDLE && currentState === userState.ACTIVE) {
updatedStatusData.idleWindowStartedAt = statusToUpdate.from ?? currentTimeStamp;
}
batch.update(docRef, updatedStatusData);
}
}
Expand Down Expand Up @@ -713,6 +735,9 @@ const cancelOooStatus = async (userId) => {
});
if (lastOooUntilUpdate !== undefined) {
newStatusData.lastOooUntil = lastOooUntilUpdate;
if (docData.currentStatus?.from != null) {
newStatusData.lastOooFrom = docData.currentStatus.from;
}
}
if (futureStatus?.state) {
newStatusData.futureStatus = {};
Expand Down
40 changes: 39 additions & 1 deletion test/unit/utils/userStatus.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
const chai = require("chai");
const { expect } = chai;
const { generateNewStatus, checkIfUserHasLiveTasks, convertTimestampsToUTC } = require("../../../utils/userStatus");
const {
generateNewStatus,
checkIfUserHasLiveTasks,
convertTimestampsToUTC,
computeIdleDaysExcludingOOO,
} = require("../../../utils/userStatus");
const { userState } = require("../../../constants/userStatus");
const {
OutputFixtureForFnConvertTimestampsToUTC,
Expand Down Expand Up @@ -102,4 +107,37 @@ describe("User Status Functions", function () {
expect(result).to.deep.equal(OutputFixtureForFnConvertTimestampsToUTC);
});
});

describe("computeIdleDaysExcludingOOO", function () {
const ONE_DAY_MS = 1000 * 60 * 60 * 24;

it("should return total idle days when no OOO period", function () {
const windowStart = Date.now() - 10 * ONE_DAY_MS;
const now = Date.now();
const days = computeIdleDaysExcludingOOO(windowStart, null, null, null, now);
expect(days).to.equal(10);
});

it("should exclude last OOO period from idle days", function () {
const windowStart = Date.now() - 15 * ONE_DAY_MS;
const oooFrom = Date.now() - 10 * ONE_DAY_MS;
const oooUntil = Date.now() - 5 * ONE_DAY_MS;
const now = Date.now();
const days = computeIdleDaysExcludingOOO(windowStart, oooFrom, oooUntil, null, now);
expect(days).to.equal(10);
});

it("should fall back to currentStatusFrom when idleWindowStartedAt is missing", function () {
const currentStatusFrom = Date.now() - 8 * ONE_DAY_MS;
const now = Date.now();
const days = computeIdleDaysExcludingOOO(null, null, null, currentStatusFrom, now);
expect(days).to.equal(8);
});

it("should return 0 when window has no span", function () {
const now = Date.now();
const days = computeIdleDaysExcludingOOO(now, null, null, null, now);
expect(days).to.equal(0);
});
});
});
32 changes: 32 additions & 0 deletions utils/userStatus.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,37 @@ const resolveLastOooUntil = ({ previousState, previousUntil, nextState, fallback
return undefined;
};

const ONE_DAY_MS = 1000 * 60 * 60 * 24;

/**
* Computes total idle days in the window [windowStart, now], excluding the last OOO period.
* Used for group-idle-7d+ so that OOO days are not counted toward the 7-day idle threshold.
*
* @param {number|string|admin.firestore.Timestamp|null|undefined} idleWindowStartedAt - When idle window started (e.g. task completed).
* @param {number|string|admin.firestore.Timestamp|null|undefined} lastOooFrom - Start of last OOO period.
* @param {number|string|admin.firestore.Timestamp|null|undefined} lastOooUntil - End of last OOO period.
* @param {number|string|admin.firestore.Timestamp|null|undefined} currentStatusFrom - Fallback window start (e.g. currentStatus.from).
* @param {number} nowMs - Reference "now" in milliseconds.
* @returns {number} Total idle days (excluding OOO) in the window.
*/
const computeIdleDaysExcludingOOO = (idleWindowStartedAt, lastOooFrom, lastOooUntil, currentStatusFrom, nowMs) => {
const windowStart = normalizeTimestamp(idleWindowStartedAt) ?? normalizeTimestamp(currentStatusFrom) ?? nowMs;
const windowEnd = nowMs;
let totalMs = Math.max(0, windowEnd - windowStart);

const oooFrom = normalizeTimestamp(lastOooFrom);
const oooUntil = normalizeTimestamp(lastOooUntil);
if (oooFrom != null && oooUntil != null && oooFrom < oooUntil) {
const overlapStart = Math.max(windowStart, oooFrom);
const overlapEnd = Math.min(windowEnd, oooUntil);
if (overlapStart < overlapEnd) {
totalMs -= overlapEnd - overlapStart;
}
}

return Math.floor(totalMs / ONE_DAY_MS);
};

/* returns the User Id based on the route path
* @param req {Object} : Express request object
* @returns userId {Number | undefined} : the user id incase it exists
Expand Down Expand Up @@ -441,4 +472,5 @@ module.exports = {
convertTimestampsToUTC,
normalizeTimestamp,
resolveLastOooUntil,
computeIdleDaysExcludingOOO,
};
Loading