feat: Set daily claim cooldown to next UTC midnight and reset streak to 1 if missed by over 24 hours.

This commit is contained in:
syntaxbullet
2026-01-05 12:10:41 +01:00
parent a227e5db59
commit fb260c5beb
2 changed files with 43 additions and 6 deletions

View File

@@ -173,10 +173,22 @@ describe("economyService", () => {
it("should throw if cooldown is active", async () => {
const future = new Date("2023-01-02T12:00:00Z"); // +24h
mockFindFirst.mockResolvedValue({ expiresAt: future });
expect(economyService.claimDaily("1")).rejects.toThrow("Daily already claimed");
});
it("should set cooldown to next UTC midnight", async () => {
// 2023-01-01T12:00:00Z -> Should be 2023-01-02T00:00:00Z
mockFindFirst
.mockResolvedValueOnce(undefined) // No cooldown
.mockResolvedValueOnce({ id: 1n, dailyStreak: 5, balance: 1000n });
const result = await economyService.claimDaily("1");
const expectedReset = new Date("2023-01-02T00:00:00Z");
expect(result.nextReadyAt.toISOString()).toBe(expectedReset.toISOString());
});
it("should reset streak if missed a day (long time gap)", async () => {
// Expired 3 days ago
const past = new Date("2023-01-01T00:00:00Z"); // now is 12:00
@@ -193,8 +205,31 @@ describe("economyService", () => {
const result = await economyService.claimDaily("1");
// timeSinceReady = 48h.
// streak = (5+1) - floor(48h / 24h) = 6 - 2 = 4.
expect(result.streak).toBe(4);
// streak should reset to 1
expect(result.streak).toBe(1);
});
it("should prevent weekly bonus exploit by resetting streak", async () => {
// Mock user at streak 7.
// Mock time as 24h + 1m after expiry.
const expiredAt = new Date("2023-01-01T11:59:00Z"); // now is 12:00 next day, plus 1 min gap?
// no, 'now' is 2023-01-01T12:00:00Z set in beforeEach
// We want gap > 24h.
// If expiry was yesterday 11:59:59. Gap is 24h + 1s.
const expiredAtExploit = new Date("2022-12-31T11:59:00Z"); // Over 24h ago
mockFindFirst
.mockResolvedValueOnce({ expiresAt: expiredAtExploit })
.mockResolvedValueOnce({ id: 1n, dailyStreak: 7 });
const result = await economyService.claimDaily("1");
// Should reset to 1
expect(result.streak).toBe(1);
expect(result.isWeekly).toBe(false);
});
});

View File

@@ -98,7 +98,7 @@ export const economyService = {
if (cooldown) {
const timeSinceReady = now.getTime() - cooldown.expiresAt.getTime();
if (timeSinceReady > 24 * 60 * 60 * 1000) {
streak = Math.max(1, streak - Math.floor(timeSinceReady / (24 * 60 * 60 * 1000)));
streak = 1;
}
} else {
streak = 1;
@@ -119,8 +119,10 @@ export const economyService = {
})
.where(eq(users.id, BigInt(userId)));
// Set new cooldown (now + 24h)
const nextReadyAt = new Date(now.getTime() + config.economy.daily.cooldownMs);
// Set new cooldown (Next UTC Midnight)
const nextReadyAt = new Date(now);
nextReadyAt.setUTCDate(nextReadyAt.getUTCDate() + 1);
nextReadyAt.setUTCHours(0, 0, 0, 0);
await txFn.insert(userTimers)
.values({