diff --git a/api/src/server.ts b/api/src/server.ts index bc15dfd..9ca3321 100644 --- a/api/src/server.ts +++ b/api/src/server.ts @@ -3395,6 +3395,27 @@ async function getLatestBudgetSession(app: any, userId: string) { }); } +async function ensureBudgetSession(app: any, userId: string, fallbackAvailableCents = 0) { + const existing = await getLatestBudgetSession(app, userId); + if (existing) return existing; + + const now = new Date(); + const start = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1, 0, 0, 0, 0)); + const end = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1, 0, 0, 0, 0)); + + return app.prisma.budgetSession.create({ + data: { + userId, + periodStart: start, + periodEnd: end, + totalBudgetCents: BigInt(Math.max(0, fallbackAvailableCents)), + allocatedCents: 0n, + fundedCents: 0n, + availableCents: BigInt(Math.max(0, fallbackAvailableCents)), + }, + }); +} + app.post("/variable-categories", mutationRateLimit, async (req, reply) => { const parsed = CatBody.safeParse(req.body); if (!parsed.success) { @@ -3527,14 +3548,13 @@ app.post("/variable-categories/rebalance", mutationRateLimit, async (req, reply) app.get("/variable-categories/manual-rebalance", async (req, reply) => { const userId = req.userId; - const session = await getLatestBudgetSession(app, userId); - if (!session) return reply.code(400).send({ ok: false, code: "NO_BUDGET_SESSION" }); - const cats = await app.prisma.variableCategory.findMany({ where: { userId }, orderBy: [{ priority: "asc" }, { name: "asc" }], select: { id: true, name: true, percent: true, isSavings: true, balanceCents: true }, }); + const totalBalance = cats.reduce((s, c) => s + Number(c.balanceCents ?? 0n), 0); + const session = await ensureBudgetSession(app, userId, totalBalance); return reply.send({ ok: true, @@ -3550,10 +3570,6 @@ app.post("/variable-categories/manual-rebalance", async (req, reply) => { return reply.code(400).send({ ok: false, code: "INVALID_BODY" }); } - const session = await getLatestBudgetSession(app, userId); - if (!session) return reply.code(400).send({ ok: false, code: "NO_BUDGET_SESSION" }); - const availableCents = Number(session.availableCents ?? 0n); - const cats = await app.prisma.variableCategory.findMany({ where: { userId }, orderBy: [{ priority: "asc" }, { name: "asc" }], @@ -3561,6 +3577,10 @@ app.post("/variable-categories/manual-rebalance", async (req, reply) => { }); if (cats.length === 0) return reply.code(400).send({ ok: false, code: "NO_CATEGORIES" }); + const totalBalance = cats.reduce((s, c) => s + Number(c.balanceCents ?? 0n), 0); + const session = await ensureBudgetSession(app, userId, totalBalance); + const availableCents = Number(session.availableCents ?? 0n); + const targetMap = new Map(); for (const t of parsed.data.targets) { if (targetMap.has(t.id)) return reply.code(400).send({ ok: false, code: "DUPLICATE_ID" });