phase 7: income, payday. and budget handling routes simplified and compacted
This commit is contained in:
140
api/src/routes/payday.ts
Normal file
140
api/src/routes/payday.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import type { FastifyPluginAsync } from "fastify";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
import { fromZonedTime } from "date-fns-tz";
|
||||
import { getUserMidnight } from "../allocator.js";
|
||||
|
||||
type RateLimitRouteOptions = {
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: number;
|
||||
timeWindow: number;
|
||||
keyGenerator?: (req: any) => string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type PaydayRoutesOptions = {
|
||||
mutationRateLimit: RateLimitRouteOptions;
|
||||
isProd: boolean;
|
||||
};
|
||||
|
||||
const paydayRoutes: FastifyPluginAsync<PaydayRoutesOptions> = async (app, opts) => {
|
||||
const logDebug = (message: string, data?: Record<string, unknown>) => {
|
||||
if (!opts.isProd) {
|
||||
app.log.info(data ?? {}, message);
|
||||
}
|
||||
};
|
||||
|
||||
app.get("/payday/status", async (req, reply) => {
|
||||
const userId = req.userId;
|
||||
const Query = z.object({
|
||||
debugNow: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
|
||||
});
|
||||
const query = Query.safeParse(req.query);
|
||||
|
||||
logDebug("Payday status check started", { userId });
|
||||
|
||||
const [user, paymentPlansCount] = await Promise.all([
|
||||
app.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
incomeType: true,
|
||||
incomeFrequency: true,
|
||||
firstIncomeDate: true,
|
||||
pendingScheduledIncome: true,
|
||||
timezone: true,
|
||||
},
|
||||
}),
|
||||
app.prisma.fixedPlan.count({
|
||||
where: {
|
||||
userId,
|
||||
paymentSchedule: { not: Prisma.DbNull },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!user) {
|
||||
if (!opts.isProd) {
|
||||
app.log.warn({ userId }, "User not found");
|
||||
}
|
||||
return reply.code(404).send({ message: "User not found" });
|
||||
}
|
||||
|
||||
logDebug("Payday user data retrieved", {
|
||||
userId,
|
||||
incomeType: user.incomeType,
|
||||
incomeFrequency: user.incomeFrequency,
|
||||
firstIncomeDate: user.firstIncomeDate?.toISOString(),
|
||||
pendingScheduledIncome: user.pendingScheduledIncome,
|
||||
paymentPlansCount,
|
||||
});
|
||||
|
||||
const hasPaymentPlans = paymentPlansCount > 0;
|
||||
const isRegularUser = user.incomeType === "regular";
|
||||
|
||||
if (!isRegularUser || !hasPaymentPlans || !user.firstIncomeDate) {
|
||||
logDebug("Payday check skipped - not applicable", {
|
||||
userId,
|
||||
isRegularUser,
|
||||
hasPaymentPlans,
|
||||
hasFirstIncomeDate: !!user.firstIncomeDate,
|
||||
});
|
||||
return {
|
||||
shouldShowOverlay: false,
|
||||
pendingScheduledIncome: false,
|
||||
nextPayday: null,
|
||||
};
|
||||
}
|
||||
|
||||
const { calculateNextPayday, isWithinPaydayWindow } = await import("../allocator.js");
|
||||
const userTimezone = user.timezone || "America/New_York";
|
||||
const debugNow = query.success ? query.data.debugNow : undefined;
|
||||
const now = debugNow
|
||||
? fromZonedTime(new Date(`${debugNow}T00:00:00`), userTimezone)
|
||||
: new Date();
|
||||
const nextPayday = calculateNextPayday(
|
||||
user.firstIncomeDate,
|
||||
user.incomeFrequency,
|
||||
now,
|
||||
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({
|
||||
where: {
|
||||
userId,
|
||||
isScheduledIncome: true,
|
||||
postedAt: {
|
||||
gte: dayStart,
|
||||
lte: dayEnd,
|
||||
},
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
logDebug("Payday calculation complete", {
|
||||
userId,
|
||||
now: now.toISOString(),
|
||||
firstIncomeDate: user.firstIncomeDate.toISOString(),
|
||||
nextPayday: nextPayday.toISOString(),
|
||||
isPayday,
|
||||
pendingScheduledIncome: user.pendingScheduledIncome,
|
||||
scheduledIncomeToday: !!scheduledIncomeToday,
|
||||
shouldShowOverlay: isPayday && !scheduledIncomeToday,
|
||||
});
|
||||
|
||||
return {
|
||||
shouldShowOverlay: isPayday && !scheduledIncomeToday,
|
||||
pendingScheduledIncome: !scheduledIncomeToday,
|
||||
nextPayday: nextPayday.toISOString(),
|
||||
};
|
||||
});
|
||||
|
||||
app.post("/payday/dismiss", opts.mutationRateLimit, async (_req, _reply) => {
|
||||
return { ok: true };
|
||||
});
|
||||
};
|
||||
|
||||
export default paydayRoutes;
|
||||
Reference in New Issue
Block a user