import { FastifyInstance } from "fastify"; import { z } from "zod"; import { previewAllocation, previewIrregularAllocation } from "../allocator.js"; const Body = z.object({ amountCents: z.number().int().nonnegative(), occurredAtISO: z.string().datetime().optional(), }); export default async function incomePreviewRoutes(app: FastifyInstance) { type PlanPreview = { id: string; name: string; dueOn: Date; totalCents: number; fundedCents: number; remainingCents: number; daysUntilDue: number; allocatedThisRun: number; isCrisis: boolean; }; type PreviewResult = { fixedAllocations: Array<{ fixedPlanId: string; amountCents: number; source?: string }>; variableAllocations: Array<{ variableCategoryId: string; amountCents: number }>; planStatesAfter: PlanPreview[]; availableBudgetAfterCents: number; remainingUnallocatedCents: number; crisis: { active: boolean; plans: Array<{ id: string; name: string; remainingCents: number; daysUntilDue: number; priority: number; allocatedCents: number }> }; }; app.post("/income/preview", async (req, reply) => { const parsed = Body.safeParse(req.body); if (!parsed.success) return reply.code(400).send({ message: "Invalid amount" }); const userId = req.userId; const user = await app.prisma.user.findUnique({ where: { id: userId }, select: { incomeType: true, fixedExpensePercentage: true }, }); let result: PreviewResult; if (user?.incomeType === "irregular") { const rawResult = await previewIrregularAllocation( app.prisma, userId, parsed.data.amountCents, user.fixedExpensePercentage ?? 40, parsed.data.occurredAtISO ); result = { fixedAllocations: rawResult.fixedAllocations, variableAllocations: rawResult.variableAllocations, planStatesAfter: rawResult.planStatesAfter, availableBudgetAfterCents: rawResult.availableBudgetCents, remainingUnallocatedCents: rawResult.remainingBudgetCents, crisis: rawResult.crisis, }; } else { const rawResult = await previewAllocation( app.prisma, userId, parsed.data.amountCents, parsed.data.occurredAtISO ); result = { fixedAllocations: rawResult.fixedAllocations, variableAllocations: rawResult.variableAllocations, planStatesAfter: rawResult.planStatesAfter, availableBudgetAfterCents: rawResult.availableBudgetAfterCents, remainingUnallocatedCents: rawResult.remainingUnallocatedCents, crisis: rawResult.crisis, }; } const fixedPreview = result.planStatesAfter.map((p) => ({ id: p.id, name: p.name, dueOn: p.dueOn.toISOString(), totalCents: p.totalCents, fundedCents: p.fundedCents, remainingCents: p.remainingCents, daysUntilDue: p.daysUntilDue, allocatedThisRun: p.allocatedThisRun, isCrisis: p.isCrisis, })); return { fixedAllocations: result.fixedAllocations, variableAllocations: result.variableAllocations, fixedPreview, availableBudgetAfterCents: result.availableBudgetAfterCents, crisis: result.crisis, unallocatedCents: result.remainingUnallocatedCents, }; }); }