first attempt at fixing over-allocation bug; fix npm audit block for deploy
This commit is contained in:
@@ -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"));
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user