diff --git a/api/src/routes/payday.ts b/api/src/routes/payday.ts index c9a39e9..681909a 100644 --- a/api/src/routes/payday.ts +++ b/api/src/routes/payday.ts @@ -4,6 +4,8 @@ import { z } from "zod"; import { fromZonedTime } from "date-fns-tz"; import { getUserMidnight } from "../allocator.js"; +const DAY_MS = 24 * 60 * 60 * 1000; + type RateLimitRouteOptions = { config: { rateLimit: { @@ -100,15 +102,42 @@ const paydayRoutes: FastifyPluginAsync = async (app, opts) userTimezone ); const isPayday = isWithinPaydayWindow(now, nextPayday, 0, userTimezone); - const dayStart = getUserMidnight(userTimezone, now); - const dayEnd = new Date(dayStart.getTime() + 24 * 60 * 60 * 1000 - 1); - const scheduledIncomeToday = await app.prisma.incomeEvent.findFirst({ + + // Determine the currently due pay cycle: + // - on payday: cycle starts today + // - after payday: cycle starts on the last expected payday + const lookbackDaysByFrequency = { + weekly: 14, + biweekly: 28, + monthly: 62, + } as const; + const lookbackAnchor = new Date( + now.getTime() - lookbackDaysByFrequency[user.incomeFrequency] * DAY_MS + ); + const currentCycleStart = calculateNextPayday( + user.firstIncomeDate, + user.incomeFrequency, + lookbackAnchor, + userTimezone + ); + const isCycleDue = now.getTime() >= currentCycleStart.getTime(); + const currentCycleEnd = + currentCycleStart.getTime() === nextPayday.getTime() + ? calculateNextPayday( + user.firstIncomeDate, + user.incomeFrequency, + new Date(nextPayday.getTime() + DAY_MS), + userTimezone + ) + : nextPayday; + + const scheduledIncomeInCurrentCycle = await app.prisma.incomeEvent.findFirst({ where: { userId, isScheduledIncome: true, postedAt: { - gte: dayStart, - lte: dayEnd, + gte: currentCycleStart, + lt: currentCycleEnd, }, }, select: { id: true }, @@ -120,14 +149,17 @@ const paydayRoutes: FastifyPluginAsync = async (app, opts) firstIncomeDate: user.firstIncomeDate.toISOString(), nextPayday: nextPayday.toISOString(), isPayday, + currentCycleStart: currentCycleStart.toISOString(), + currentCycleEnd: currentCycleEnd.toISOString(), + isCycleDue, pendingScheduledIncome: user.pendingScheduledIncome, - scheduledIncomeToday: !!scheduledIncomeToday, - shouldShowOverlay: isPayday && !scheduledIncomeToday, + scheduledIncomeInCurrentCycle: !!scheduledIncomeInCurrentCycle, + shouldShowOverlay: isCycleDue && !scheduledIncomeInCurrentCycle, }); return { - shouldShowOverlay: isPayday && !scheduledIncomeToday, - pendingScheduledIncome: !scheduledIncomeToday, + shouldShowOverlay: isCycleDue && !scheduledIncomeInCurrentCycle, + pendingScheduledIncome: !scheduledIncomeInCurrentCycle, nextPayday: nextPayday.toISOString(), }; });