fix: fix bug in rebalanace, stale session values being read
All checks were successful
Deploy / deploy (push) Successful in 1m30s
Security Tests / security-non-db (push) Successful in 20s
Security Tests / security-db (push) Successful in 24s

This commit is contained in:
2026-03-12 13:51:46 -05:00
parent a03fbea5e7
commit 58545b2da7
3 changed files with 52 additions and 7 deletions

6
.env
View File

@@ -31,9 +31,9 @@ EMAIL_FROM="SkyMoney Budget <no-reply@skymoneybudget.com>"
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

View File

@@ -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<string, number>();
for (const t of parsed.data.targets) {

View File

@@ -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