diff --git a/backend/src/cronjobs/cronjobs.types.ts b/backend/src/cronjobs/cronjobs.types.ts index 71a09b6b..ab92eea5 100644 --- a/backend/src/cronjobs/cronjobs.types.ts +++ b/backend/src/cronjobs/cronjobs.types.ts @@ -25,6 +25,7 @@ export type ReceivableUser = Pick< | "id" | "ghostModeRemindersEmail" | "restrictedViewToken" + | "lastDateModeReminderSent" >; export enum TimeSpan { @@ -40,10 +41,25 @@ export interface OfflineUserSince { export const goBackInTimeFor = ( value: number, - unit: "hours" | "days", + unit: "hours" | "days" | "months" | "years", ): Date => { - const hours = unit === "days" ? value * 24 : value; - return new Date(new Date().getTime() - hours * 60 * 60 * 1000); + const now = new Date(); + switch (unit) { + case "hours": + return new Date(now.getTime() - value * 60 * 60 * 1000); + case "days": + return new Date(now.getTime() - value * 24 * 60 * 60 * 1000); + case "months": { + const date = new Date(now); + date.setMonth(date.getMonth() - value); + return date; + } + case "years": { + const date = new Date(now); + date.setFullYear(date.getFullYear() - value); + return date; + } + } }; /** @dev String value used for translation keys */ diff --git a/backend/src/cronjobs/ghostmode-reminder.cron.ts b/backend/src/cronjobs/ghostmode-reminder.cron.ts index 79571fea..f6644ee9 100644 --- a/backend/src/cronjobs/ghostmode-reminder.cron.ts +++ b/backend/src/cronjobs/ghostmode-reminder.cron.ts @@ -6,6 +6,7 @@ import { goBackInTimeFor, IntervalHour, OfflineUserSince, + ReceivableUser, TimeSpan, } from "@/cronjobs/cronjobs.types"; import { ENotificationType } from "@/DTOs/abstract/base-notification.adto"; @@ -57,24 +58,27 @@ export class GhostModeReminderCronJob extends BaseCronJob { "user.lastDateModeChange", "user.ghostModeRemindersEmail", "user.restrictedViewToken", + "user.lastDateModeReminderSent", ]) .where("user.dateMode = :mode", { mode: EDateMode.GHOST }) .andWhere("user.lastDateModeChange < :dayAgo", { dayAgo: goBackInTimeFor(24, "hours"), }) .andWhere( - "(user.lastDateModeReminderSent IS NULL OR " + + "(user.lastDateModeReminderSent IS NULL) OR " + // Reminded if > 24 hrs off and never reminded + "(user.lastDateModeReminderSent < user.lastDateModeChange) OR " + // Remind if went online in-between "CASE " + - "WHEN user.lastDateModeChange < :twoWeeksAgo THEN user.lastDateModeReminderSent < :twoWeeksMinTime " + - "WHEN user.lastDateModeChange < :threeDaysAgo THEN user.lastDateModeReminderSent < :threeDaysMinTime " + - "ELSE user.lastDateModeReminderSent < :oneDayMinTime " + - "END)", + "WHEN user.lastDateModeChange < :twoWeeksAgo AND user.lastDateModeReminderSent >= :twoWeeksMinTime THEN 0 " + + "WHEN user.lastDateModeChange < :threeDaysAgo AND user.lastDateModeReminderSent < :threeDaysMinTime AND user.lastDateModeReminderSent > :twoWeeksAgo THEN 1 " + + "WHEN user.lastDateModeChange < :oneDayAgo AND user.lastDateModeReminderSent IS NULL THEN 1 " + + "ELSE 0 " + + "END = 1", { twoWeeksAgo: goBackInTimeFor(336, "hours"), threeDaysAgo: goBackInTimeFor(72, "hours"), + oneDayAgo: goBackInTimeFor(24, "hours"), twoWeeksMinTime: goBackInTimeFor(336 - 72, "hours"), threeDaysMinTime: goBackInTimeFor(72 - 24, "hours"), - oneDayMinTime: goBackInTimeFor(24, "hours"), }, ); @@ -92,15 +96,22 @@ export class GhostModeReminderCronJob extends BaseCronJob { return users.map((user) => ({ user, - type: this.determineOfflineType(user.lastDateModeChange), + type: this.determineOfflineType(user, user.lastDateModeChange), })); } - private determineOfflineType(lastDateModeChange: Date): TimeSpan { + private determineOfflineType( + user: ReceivableUser, + lastDateModeChange: Date, + ): TimeSpan { const hoursGhostMode = differenceInHours( new Date(), lastDateModeChange, ); + /** @DEV Edge case: if a user has never been reminded he is always in the 24 hrs bucket */ + if (!user.lastDateModeReminderSent) { + return TimeSpan.ONE_DAY; + } if (hoursGhostMode >= 336) return TimeSpan.TWO_WEEKS; if (hoursGhostMode >= 72) return TimeSpan.THREE_DAYS; return TimeSpan.ONE_DAY; diff --git a/backend/test/integration/cronjobs/ghostmode-reminder.cron.spec.ts b/backend/test/integration/cronjobs/ghostmode-reminder.cron.spec.ts index 93152b99..db794eef 100644 --- a/backend/test/integration/cronjobs/ghostmode-reminder.cron.spec.ts +++ b/backend/test/integration/cronjobs/ghostmode-reminder.cron.spec.ts @@ -25,7 +25,7 @@ describe("CronJob: GhostMode Reminder", () => { userRepository = module.get>(getRepositoryToken(User)); }); - describe("Ghost Mode Reminder: Happy Path Tests", function () { + describe("Happy Path Tests", function () { it("should identify users for ONE_DAY reminder correctly", async () => { const user = await userFactory.persistNewTestUser({ ...baseUser, @@ -40,8 +40,8 @@ describe("CronJob: GhostMode Reminder", () => { it("should identify users for THREE_DAYS reminder correctly", async () => { const user = await userFactory.persistNewTestUser({ ...baseUser, - lastDateModeChange: goBackInTimeFor(73, "hours"), - lastDateModeReminderSent: goBackInTimeFor(49, "hours"), + lastDateModeChange: goBackInTimeFor(72, "hours"), + lastDateModeReminderSent: goBackInTimeFor(48, "hours"), }); const result = await service.findOfflineUsers(); @@ -51,8 +51,8 @@ describe("CronJob: GhostMode Reminder", () => { it("should identify users for TWO_WEEKS reminder correctly", async () => { const user = await userFactory.persistNewTestUser({ ...baseUser, - lastDateModeChange: goBackInTimeFor(337, "hours"), - lastDateModeReminderSent: goBackInTimeFor(265, "hours"), + lastDateModeChange: goBackInTimeFor(336, "hours"), + lastDateModeReminderSent: goBackInTimeFor(264, "hours"), }); const result = await service.findOfflineUsers(); @@ -61,18 +61,259 @@ describe("CronJob: GhostMode Reminder", () => { }); }); - describe("Ghost Mode: Border Cases", () => { - it("should not remind 24h users if already reminded within 24h", async () => { - await userFactory.persistNewTestUser({ - ...baseUser, - lastDateModeChange: goBackInTimeFor(25, "hours"), - lastDateModeReminderSent: goBackInTimeFor(23, "hours"), + describe("24/72/336 Cycle Test", function () { + describe("24 hours cycle - First Reminder", function () { + it("should not remind if 23 hrs offline and never reminded", async () => { + await userFactory.persistNewTestUser({ + ...baseUser, + lastDateModeChange: goBackInTimeFor(23, "hours"), + lastDateModeReminderSent: null, + }); + + const result = await service.findOfflineUsers(); + expect(result.length).toEqual(0); }); + it("should not remind if 23.9 hrs offline and never reminded", async () => { + await userFactory.persistNewTestUser({ + ...baseUser, + lastDateModeChange: goBackInTimeFor(23.9, "hours"), + lastDateModeReminderSent: null, + }); - const result = await service.findOfflineUsers(); - expect(result.length).toEqual(0); + const result = await service.findOfflineUsers(); + expect(result.length).toEqual(0); + }); + it("should not remind if 30 hrs offline and never reminded", async () => { + await userFactory.persistNewTestUser({ + ...baseUser, + lastDateModeChange: goBackInTimeFor(23.9, "hours"), + lastDateModeReminderSent: null, + }); + + const result = await service.findOfflineUsers(); + expect(result.length).toEqual(0); + }); + it("should remind if 24 hrs offline and never reminded", async () => { + await userFactory.persistNewTestUser({ + ...baseUser, + lastDateModeChange: goBackInTimeFor(24, "hours"), + lastDateModeReminderSent: null, + }); + + const result = await service.findOfflineUsers(); + expect(result.length).toEqual(1); + expect(result[0].type).toEqual(TimeSpan.ONE_DAY); + }); + it("should remind if 24.1 hrs offline never reminded", async () => { + await userFactory.persistNewTestUser({ + ...baseUser, + lastDateModeChange: goBackInTimeFor(24.1, "hours"), + lastDateModeReminderSent: null, + }); + + const result = await service.findOfflineUsers(); + expect(result.length).toEqual(1); + expect(result[0].type).toEqual(TimeSpan.ONE_DAY); + }); + it("should remind if 30 hrs offline never reminded", async () => { + await userFactory.persistNewTestUser({ + ...baseUser, + lastDateModeChange: goBackInTimeFor(30, "hours"), + lastDateModeReminderSent: null, + }); + + const result = await service.findOfflineUsers(); + expect(result.length).toEqual(1); + expect(result[0].type).toEqual(TimeSpan.ONE_DAY); + }); + it("should remind if 30 hrs offline reminded a week ago", async () => { + await userFactory.persistNewTestUser({ + ...baseUser, + lastDateModeChange: goBackInTimeFor(30, "hours"), + lastDateModeReminderSent: goBackInTimeFor(7, "days"), + }); + + const result = await service.findOfflineUsers(); + expect(result.length).toEqual(1); + expect(result[0].type).toEqual(TimeSpan.ONE_DAY); + }); + it("should remind if 100 hrs offline and never reminded", async () => { + await userFactory.persistNewTestUser({ + ...baseUser, + lastDateModeChange: goBackInTimeFor(100, "hours"), + lastDateModeReminderSent: null, + }); + + const result = await service.findOfflineUsers(); + expect(result[0].type).toEqual(TimeSpan.ONE_DAY); + expect(result.length).toEqual(1); + }); + }); + describe("72 hours cycle - Second Reminder", function () { + it("should not remind if 3 days offline and last reminded 1 day ago", async () => { + await userFactory.persistNewTestUser({ + ...baseUser, + lastDateModeChange: goBackInTimeFor(3, "days"), + lastDateModeReminderSent: goBackInTimeFor(1, "days"), + }); + + const result = await service.findOfflineUsers(); + expect(result.length).toEqual(0); + }); + it("should not remind if 72 hrs offline and last reminded 12 hrs ago", async () => { + await userFactory.persistNewTestUser({ + ...baseUser, + lastDateModeChange: goBackInTimeFor(72, "hours"), + lastDateModeReminderSent: goBackInTimeFor(12, "hours"), + }); + + const result = await service.findOfflineUsers(); + expect(result.length).toEqual(0); + }); + it("should remind if 35 hrs offline and last reminded 5 days ago", async () => { + await userFactory.persistNewTestUser({ + ...baseUser, + lastDateModeChange: goBackInTimeFor(35, "hours"), + lastDateModeReminderSent: goBackInTimeFor(72, "hours"), + }); + + const result = await service.findOfflineUsers(); + expect(result.length).toEqual(1); + expect(result[0].type).toEqual(TimeSpan.ONE_DAY); + }); + it("should remind if 72 hrs offline and last reminded 48 hrs ago", async () => { + await userFactory.persistNewTestUser({ + ...baseUser, + lastDateModeChange: goBackInTimeFor(72, "hours"), + lastDateModeReminderSent: goBackInTimeFor(48, "hours"), + }); + + const result = await service.findOfflineUsers(); + expect(result[0].type).toEqual(TimeSpan.THREE_DAYS); + expect(result.length).toEqual(1); + }); + it("should remind if 72 hrs offline and last reminded 120 hrs ago", async () => { + await userFactory.persistNewTestUser({ + ...baseUser, + lastDateModeChange: goBackInTimeFor(72, "hours"), + lastDateModeReminderSent: goBackInTimeFor(120, "hours"), + }); + + const result = await service.findOfflineUsers(); + expect(result[0].type).toEqual(TimeSpan.THREE_DAYS); + expect(result.length).toEqual(1); + }); + it("should remind if 72 hrs offline and never reminded", async () => { + await userFactory.persistNewTestUser({ + ...baseUser, + lastDateModeChange: goBackInTimeFor(72, "hours"), + lastDateModeReminderSent: null, + }); + + const result = await service.findOfflineUsers(); + expect(result[0].type).toEqual(TimeSpan.ONE_DAY); + expect(result.length).toEqual(1); + }); }); - it("should not include users in multiple buckets", async () => { + describe("335 hours cycle - Third Reminder", function () { + it("should not remind if 30 days offline and last reminded 10 days ago", async () => { + const user = await userFactory.persistNewTestUser({ + ...baseUser, + lastDateModeChange: goBackInTimeFor(30, "days"), + lastDateModeReminderSent: goBackInTimeFor(10, "days"), + }); + + console.log({ + lastModeChange: user.lastDateModeChange, + lastReminder: user.lastDateModeReminderSent, + twoWeeksAgo: goBackInTimeFor(336, "hours"), + twoWeeksMinTime: goBackInTimeFor(336 - 72, "hours"), + }); + + const result = await service.findOfflineUsers(); + expect(result.length).toEqual(0); + }); + it("should remind if 73 hrs offline and last reminded 263 hrs ago", async () => { + await userFactory.persistNewTestUser({ + ...baseUser, + lastDateModeChange: goBackInTimeFor(70, "hours"), + lastDateModeReminderSent: goBackInTimeFor(260, "hours"), // was reminded 11 days ago + }); + const result = await service.findOfflineUsers(); + expect(result.length).toEqual(1); + expect(result[0].type).toEqual(TimeSpan.ONE_DAY); + }); + it("should remind if 336 hrs offline and last reminded 264 hrs ago", async () => { + await userFactory.persistNewTestUser({ + ...baseUser, + lastDateModeChange: goBackInTimeFor(336, "hours"), + lastDateModeReminderSent: goBackInTimeFor( + 336 - 72, // 264 + "hours", + ), + }); + const result = await service.findOfflineUsers(); + expect(result.length).toEqual(1); + expect(result[0].type).toEqual(TimeSpan.TWO_WEEKS); + }); + it("should remind if 336 hrs offline and last reminded 265 hrs ago", async () => { + await userFactory.persistNewTestUser({ + ...baseUser, + lastDateModeChange: goBackInTimeFor(336, "hours"), + lastDateModeReminderSent: goBackInTimeFor( + 336 - 71, // 265 + "hours", + ), + }); + const result = await service.findOfflineUsers(); + expect(result.length).toEqual(1); + expect(result[0].type).toEqual(TimeSpan.TWO_WEEKS); + }); + it("should remind if 336 hrs offline and never reminded", async () => { + await userFactory.persistNewTestUser({ + ...baseUser, + lastDateModeChange: goBackInTimeFor(336, "hours"), + lastDateModeReminderSent: null, + }); + const result = await service.findOfflineUsers(); + expect(result[0].type).toEqual(TimeSpan.ONE_DAY); + expect(result.length).toEqual(1); + }); + it("should remind if 450 hrs offline and never reminded", async () => { + await userFactory.persistNewTestUser({ + ...baseUser, + lastDateModeChange: goBackInTimeFor(450, "hours"), + lastDateModeReminderSent: null, + }); + const result = await service.findOfflineUsers(); + expect(result[0].type).toEqual(TimeSpan.ONE_DAY); + expect(result.length).toEqual(1); + }); + it("should remind if 450 hrs offline and last reminded 264 hrs ago", async () => { + await userFactory.persistNewTestUser({ + ...baseUser, + lastDateModeChange: goBackInTimeFor(450, "hours"), + lastDateModeReminderSent: goBackInTimeFor(264, "hours"), + }); + const result = await service.findOfflineUsers(); + expect(result[0].type).toEqual(TimeSpan.TWO_WEEKS); + expect(result.length).toEqual(1); + }); + it("should remind if 450 hrs offline and last reminded 265 hrs ago", async () => { + await userFactory.persistNewTestUser({ + ...baseUser, + lastDateModeChange: goBackInTimeFor(450, "hours"), + lastDateModeReminderSent: goBackInTimeFor(265, "hours"), + }); + const result = await service.findOfflineUsers(); + expect(result[0].type).toEqual(TimeSpan.TWO_WEEKS); + expect(result.length).toEqual(1); + }); + }); + }); + + describe("Edge Cases", () => { + it("should not remind users twice if service runs twice", async () => { const user = await userFactory.persistNewTestUser({ ...baseUser, lastDateModeChange: goBackInTimeFor(337, "hours"), @@ -87,7 +328,6 @@ describe("CronJob: GhostMode Reminder", () => { expect(afterResult).toEqual([]); }); it("should correctly handle multiple users in different time buckets", async () => { - // ONE_DAY users await userFactory.persistNewTestUser({ ...baseUser, lastDateModeChange: goBackInTimeFor(25, "hours"), @@ -98,38 +338,30 @@ describe("CronJob: GhostMode Reminder", () => { lastDateModeChange: goBackInTimeFor(30, "hours"), lastDateModeReminderSent: null, }); - - // THREE_DAYS users await userFactory.persistNewTestUser({ ...baseUser, lastDateModeChange: goBackInTimeFor(73, "hours"), lastDateModeReminderSent: goBackInTimeFor(49, "hours"), }); - await userFactory.persistNewTestUser({ - ...baseUser, - lastDateModeChange: goBackInTimeFor(100, "hours"), - lastDateModeReminderSent: null, - }); - - // TWO_WEEKS users await userFactory.persistNewTestUser({ ...baseUser, lastDateModeChange: goBackInTimeFor(337, "hours"), lastDateModeReminderSent: goBackInTimeFor(265, "hours"), }); - const result = await service.findOfflineUsers(); - - // Check counts per bucket - const oneDayUsers = result.filter((u) => u.type === "ONE_DAY"); - const threeDayUsers = result.filter((u) => u.type === "THREE_DAYS"); - const twoWeekUsers = result.filter((u) => u.type === "TWO_WEEKS"); + const oneDayUsers = result.filter( + (u) => u.type === TimeSpan.ONE_DAY, + ); + const threeDayUsers = result.filter( + (u) => u.type === TimeSpan.THREE_DAYS, + ); + const twoWeekUsers = result.filter( + (u) => u.type === TimeSpan.TWO_WEEKS, + ); expect(oneDayUsers).toHaveLength(2); - expect(threeDayUsers).toHaveLength(2); + expect(threeDayUsers).toHaveLength(1); expect(twoWeekUsers).toHaveLength(1); - - // Run again and verify no users are returned (they've been reminded) const secondResult = await service.findOfflineUsers(); expect(secondResult).toHaveLength(0); }); @@ -159,25 +391,22 @@ describe("CronJob: GhostMode Reminder", () => { expect(result).toHaveLength(3); const types = result.map((u) => u.type); - expect(types).toContain("ONE_DAY"); - expect(types).toContain("THREE_DAYS"); - expect(types).toContain("TWO_WEEKS"); + expect(types).toContain(TimeSpan.ONE_DAY); + expect(types).toContain(TimeSpan.ONE_DAY); + expect(types).toContain(TimeSpan.ONE_DAY); }); it("should not include users who were recently reminded", async () => { await Promise.all([ - // Reminded 23 hours ago (should not be included) userFactory.persistNewTestUser({ ...baseUser, lastDateModeChange: goBackInTimeFor(25, "hours"), lastDateModeReminderSent: goBackInTimeFor(23, "hours"), }), - // Reminded 47 hours ago (should not be included) userFactory.persistNewTestUser({ ...baseUser, lastDateModeChange: goBackInTimeFor(73, "hours"), lastDateModeReminderSent: goBackInTimeFor(47, "hours"), }), - // Reminded 263 hours ago (should not be included) userFactory.persistNewTestUser({ ...baseUser, lastDateModeChange: goBackInTimeFor(337, "hours"), @@ -190,31 +419,26 @@ describe("CronJob: GhostMode Reminder", () => { }); it("should handle mixed scenarios of reminded and never reminded users", async () => { await Promise.all([ - // ONE_DAY - never reminded userFactory.persistNewTestUser({ ...baseUser, lastDateModeChange: goBackInTimeFor(25, "hours"), lastDateModeReminderSent: null, }), - // ONE_DAY - recently reminded (should not be included) userFactory.persistNewTestUser({ ...baseUser, lastDateModeChange: goBackInTimeFor(25, "hours"), lastDateModeReminderSent: goBackInTimeFor(23, "hours"), }), - // THREE_DAYS - never reminded userFactory.persistNewTestUser({ ...baseUser, lastDateModeChange: goBackInTimeFor(73, "hours"), lastDateModeReminderSent: null, }), - // THREE_DAYS - recently reminded (should not be included) userFactory.persistNewTestUser({ ...baseUser, lastDateModeChange: goBackInTimeFor(73, "hours"), lastDateModeReminderSent: goBackInTimeFor(47, "hours"), }), - // TWO_WEEKS - never reminded userFactory.persistNewTestUser({ ...baseUser, lastDateModeChange: goBackInTimeFor(337, "hours"), @@ -225,51 +449,81 @@ describe("CronJob: GhostMode Reminder", () => { const result = await service.findOfflineUsers(); expect(result).toHaveLength(3); - // Verify correct distribution const types = result.map((u) => u.type); - expect(types.filter((t) => t === "ONE_DAY")).toHaveLength(1); - expect(types.filter((t) => t === "THREE_DAYS")).toHaveLength(1); - expect(types.filter((t) => t === "TWO_WEEKS")).toHaveLength(1); + expect(types.filter((t) => t === TimeSpan.ONE_DAY)).toHaveLength(3); + expect(types.filter((t) => t === TimeSpan.THREE_DAYS)).toHaveLength( + 0, + ); + expect(types.filter((t) => t === TimeSpan.TWO_WEEKS)).toHaveLength( + 0, + ); + }); + it("should restart if user went online in-between - variation 1", async () => { + /** @DEV - it should not matter, how long he was off if he has never been reminded -> should put him in reminder 1! */ + await userFactory.persistNewTestUser({ + ...baseUser, + lastDateModeChange: goBackInTimeFor(3, "days"), // was only 3 days ago + lastDateModeReminderSent: goBackInTimeFor(2, "months"), // last reminded 2 months ago + }); + + const result = await service.findOfflineUsers(); + expect(result).toHaveLength(1); + }); + it("should restart if user went online in-between - variation 2", async () => { + /** @DEV - it should not matter, how long he was off if he has never been reminded -> should put him in reminder 1! */ + await userFactory.persistNewTestUser({ + ...baseUser, + lastDateModeChange: goBackInTimeFor(1, "days"), // was only 3 days ago + lastDateModeReminderSent: goBackInTimeFor(7, "days"), // last reminded 2 months ago + }); + + const result = await service.findOfflineUsers(); + expect(result).toHaveLength(1); + }); + it("should not restart if user never went back online - variation 3", async () => { + /** @DEV - it should not matter, how long he was off if he has never been reminded -> should put him in reminder 1! */ + await userFactory.persistNewTestUser({ + ...baseUser, + lastDateModeChange: goBackInTimeFor(2, "months"), // was only 3 days ago + lastDateModeReminderSent: goBackInTimeFor(14, "days"), // last reminded 2 months ago + }); + + const result = await service.findOfflineUsers(); + expect(result).toHaveLength(0); }); }); - describe("Ghost Mode: Special Tests, Progression", function () { + describe("Special Tests, Progression", function () { it("should handle progression through reminder stages", async () => { // Create a user that will progress through all stages const user = await userFactory.persistNewTestUser({ ...baseUser, - lastDateModeChange: goBackInTimeFor(337, "hours"), // Qualifies for TWO_WEEKS + lastDateModeChange: goBackInTimeFor(24, "hours"), lastDateModeReminderSent: null, }); - // First run - should get TWO_WEEKS reminder + // First run const firstResult = await service.findOfflineUsers(); expect(firstResult).toHaveLength(1); - expect(firstResult[0].type).toBe("TWO_WEEKS"); + expect(firstResult[0].type).toBe(TimeSpan.ONE_DAY); - // Update user's lastDateModeChange to qualify for THREE_DAYS + // Second run await userRepository.update(user.id, { lastDateModeChange: goBackInTimeFor(73, "hours"), lastDateModeReminderSent: goBackInTimeFor(49, "hours"), }); - - // Second run - should get THREE_DAYS reminder const secondResult = await service.findOfflineUsers(); expect(secondResult).toHaveLength(1); - expect(secondResult[0].type).toBe("THREE_DAYS"); + expect(secondResult[0].type).toBe(TimeSpan.THREE_DAYS); - // Update user's lastDateModeChange to qualify for ONE_DAY + // Third run await userRepository.update(user.id, { - lastDateModeChange: goBackInTimeFor(25, "hours"), - lastDateModeReminderSent: null, + lastDateModeChange: goBackInTimeFor(336, "hours"), + lastDateModeReminderSent: goBackInTimeFor(336 - 72, "hours"), }); - - // Third run - should get ONE_DAY reminder const thirdResult = await service.findOfflineUsers(); expect(thirdResult).toHaveLength(1); - expect(thirdResult[0].type).toBe("ONE_DAY"); - - // Final run - should get no reminders (already reminded) + expect(thirdResult[0].type).toBe(TimeSpan.TWO_WEEKS); const finalResult = await service.findOfflineUsers(); expect(finalResult).toHaveLength(0); });