Files
SkyMoney/api/src/routes/income-preview.ts

99 lines
3.2 KiB
TypeScript

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,
};
});
}