first attempt at fixing over-allocation bug; fix npm audit block for deploy
All checks were successful
Deploy / deploy (push) Successful in 1m29s
Security Tests / security-non-db (push) Successful in 23s
Security Tests / security-db (push) Successful in 33s

This commit is contained in:
2026-04-02 22:06:35 -05:00
parent b3d32c08e9
commit 47bc092da1
3 changed files with 77 additions and 4 deletions

View File

@@ -15,7 +15,7 @@ jobs:
set -euo pipefail
cd api
npm ci
npm audit --omit=dev --json > /tmp/skymoney-api-audit.json || true
npm audit --omit=dev --audit-level=high --json > /tmp/skymoney-api-audit.json || true
node -e '
const fs = require("fs");
const report = JSON.parse(fs.readFileSync("/tmp/skymoney-api-audit.json", "utf8"));

View File

@@ -778,8 +778,20 @@ async function applyAllocations(
): Promise<Array<{id: string, name: string, dueOn: Date}>> {
// Fixed plans
const planUpdates = new Map<string, number>();
const fixedAllocationRows = new Map<string, { planId: string; source: FixedAllocation["source"]; amountCents: number }>();
result.fixedAllocations.forEach((a) => {
planUpdates.set(a.fixedPlanId, (planUpdates.get(a.fixedPlanId) ?? 0) + a.amountCents);
const rowKey = `${a.fixedPlanId}:${a.source}`;
const existing = fixedAllocationRows.get(rowKey);
if (existing) {
existing.amountCents += a.amountCents;
return;
}
fixedAllocationRows.set(rowKey, {
planId: a.fixedPlanId,
source: a.source,
amountCents: a.amountCents,
});
});
const fullyFundedPlans: Array<{id: string, name: string, dueOn: Date}> = [];
@@ -834,13 +846,19 @@ async function applyAllocations(
needsFundingThisPeriod: markFundedThisPeriod ? false : !isFullyFunded,
},
});
}
for (const row of fixedAllocationRows.values()) {
const rowAmount = Math.max(0, Math.floor(row.amountCents | 0));
if (rowAmount <= 0) continue;
await tx.allocation.create({
data: {
userId,
kind: "fixed",
toId: planId,
amountCents: BigInt(amt),
incomeId,
toId: row.planId,
amountCents: BigInt(rowAmount),
// Available-budget pulls must not be attributed to the triggering income event.
incomeId: row.source === "income" ? incomeId : null,
},
});
}

View File

@@ -3,6 +3,8 @@ import { describe, it, expect, beforeEach, afterAll } from "vitest";
import { prisma, resetUser, ensureUser, U, cid, pid, closePrisma } from "./helpers";
import { allocateIncome, buildPlanStates } from "../src/allocator";
const DAY_MS = 86_400_000;
describe("allocator — new funding system", () => {
beforeEach(async () => {
await resetUser(U);
@@ -228,5 +230,58 @@ describe("allocator — new funding system", () => {
const sum = variable.reduce((s, a: any) => s + (a.amountCents ?? 0), 0);
expect(sum).toBe(0);
});
it("does not over-attribute crisis available-budget pulls to the triggering income event", async () => {
const c1 = cid("bucket");
await prisma.variableCategory.create({
data: { id: c1, userId: U, name: "Bucket", percent: 100, priority: 1, isSavings: false, balanceCents: 0n },
});
const planId = pid("crisis");
await prisma.fixedPlan.create({
data: {
id: planId,
userId: U,
name: "Urgent Rent",
cycleStart: new Date(Date.now() - DAY_MS).toISOString(),
dueOn: new Date(Date.now() + 3 * DAY_MS).toISOString(),
totalCents: 40000n,
fundedCents: 0n,
currentFundedCents: 0n,
lastFundingDate: null,
priority: 1,
fundingMode: "auto-on-deposit",
},
});
// Seed existing available budget from prior income that was not fully allocated.
await prisma.incomeEvent.create({
data: {
id: `seed-${Date.now()}`,
userId: U,
postedAt: new Date(Date.now() - 10 * DAY_MS),
amountCents: 50000n,
isScheduledIncome: false,
},
});
const incomeId = `inc-${Date.now()}`;
const result = await allocateIncome(prisma as any, U, 1000, new Date().toISOString(), incomeId);
expect(result.crisis.pulledFromAvailableCents).toBeGreaterThan(0);
const linkedToIncome = await prisma.allocation.findMany({
where: { userId: U, incomeId },
select: { amountCents: true, kind: true },
});
const linkedSum = linkedToIncome.reduce((sum, row) => sum + Number(row.amountCents ?? 0n), 0);
expect(linkedSum).toBeLessThanOrEqual(1000);
const unlinkedFixed = await prisma.allocation.findMany({
where: { userId: U, incomeId: null, kind: "fixed" },
select: { amountCents: true },
});
const unlinkedFixedSum = unlinkedFixed.reduce((sum, row) => sum + Number(row.amountCents ?? 0n), 0);
expect(unlinkedFixedSum).toBeGreaterThan(0);
});
});
});