From 58545b2da7928feb0142fb1e2566c1fae0e3f7a9 Mon Sep 17 00:00:00 2001 From: Ricearoni1245 Date: Thu, 12 Mar 2026 13:51:46 -0500 Subject: [PATCH] fix: fix bug in rebalanace, stale session values being read --- .env | 6 ++-- api/src/server.ts | 22 ++++++++++--- ...riable-categories.manual-rebalance.test.ts | 31 +++++++++++++++++++ 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/.env b/.env index 8a01601..1cbf440 100644 --- a/.env +++ b/.env @@ -31,9 +31,9 @@ EMAIL_FROM="SkyMoney Budget " EMAIL_BOUNCE_FROM=bounces@skymoneybudget.com EMAIL_REPLY_TO=support@skymoneybudget.com -UPDATE_NOTICE_VERSION=8 -UPDATE_NOTICE_TITLE="Rebalance tools and onboarding fixes" -UPDATE_NOTICE_BODY="This release adds a dedicated Rebalance page for variable budgets with apply-time safeguard confirmations, plus onboarding fixed-expense submission and money input improvements." +UPDATE_NOTICE_VERSION=9 +UPDATE_NOTICE_TITLE="Rebalance tool bug correction" +UPDATE_NOTICE_BODY="Fixed bug issue where available budget value is stale in user sessions and wont read updated value properly." ALLOW_INSECURE_AUTH_FOR_DEV=false JWT_ISSUER=skymoney-api JWT_AUDIENCE=skymoney-web diff --git a/api/src/server.ts b/api/src/server.ts index c059feb..9570373 100644 --- a/api/src/server.ts +++ b/api/src/server.ts @@ -3417,6 +3417,20 @@ async function ensureBudgetSession(app: any, userId: string, fallbackAvailableCe }); } +async function ensureBudgetSessionAvailableSynced( + app: any, + userId: string, + availableCents: number +) { + const normalizedAvailableCents = BigInt(Math.max(0, Math.trunc(availableCents))); + const session = await ensureBudgetSession(app, userId, Number(normalizedAvailableCents)); + if ((session.availableCents ?? 0n) === normalizedAvailableCents) return session; + return app.prisma.budgetSession.update({ + where: { id: session.id }, + data: { availableCents: normalizedAvailableCents }, + }); +} + app.post("/variable-categories", mutationRateLimit, async (req, reply) => { const parsed = CatBody.safeParse(req.body); if (!parsed.success) { @@ -3555,11 +3569,11 @@ app.get("/variable-categories/manual-rebalance", async (req, reply) => { 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); + await ensureBudgetSessionAvailableSynced(app, userId, totalBalance); return reply.send({ ok: true, - availableCents: Number(session.availableCents ?? 0n), + availableCents: totalBalance, categories: cats.map((c) => ({ ...c, balanceCents: Number(c.balanceCents ?? 0n) })), }); }); @@ -3579,8 +3593,8 @@ 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 availableCents = totalBalance; + await ensureBudgetSessionAvailableSynced(app, userId, availableCents); const targetMap = new Map(); for (const t of parsed.data.targets) { diff --git a/api/tests/variable-categories.manual-rebalance.test.ts b/api/tests/variable-categories.manual-rebalance.test.ts index 60385b5..f25ae15 100644 --- a/api/tests/variable-categories.manual-rebalance.test.ts +++ b/api/tests/variable-categories.manual-rebalance.test.ts @@ -44,6 +44,37 @@ describe("manual rebalance", () => { await seedBasics(); }); + it("uses live category balances when session availableCents is stale", async () => { + await prisma.budgetSession.updateMany({ + where: { userId: U }, + data: { availableCents: 1234n }, + }); + + const getRes = await request(app.server) + .get("/variable-categories/manual-rebalance") + .set("x-user-id", U); + + expect(getRes.statusCode).toBe(200); + expect(getRes.body?.availableCents).toBe(10_000); + + const cats = await prisma.variableCategory.findMany({ where: { userId: U }, orderBy: { priority: "asc" } }); + const targets = cats.map((c) => ({ id: c.id, targetCents: 2500 })); // 4 * 2500 = 10000 + const postRes = await request(app.server) + .post("/variable-categories/manual-rebalance") + .set("x-user-id", U) + .send({ targets }); + + expect(postRes.statusCode).toBe(200); + expect(postRes.body?.availableCents).toBe(10_000); + + const session = await prisma.budgetSession.findFirst({ + where: { userId: U }, + orderBy: { periodStart: "desc" }, + select: { availableCents: true }, + }); + expect(Number(session?.availableCents ?? 0n)).toBe(10_000); + }); + it("rebalances when sums match available", async () => { const cats = await prisma.variableCategory.findMany({ where: { userId: U }, orderBy: { priority: "asc" } }); const targets = cats.map((c) => ({ id: c.id, targetCents: 2500 })); // 4 * 2500 = 10000