99 lines
3.2 KiB
TypeScript
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,
|
|
};
|
|
});
|
|
}
|