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
|
set -euo pipefail
|
||||||
cd api
|
cd api
|
||||||
npm ci
|
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 '
|
node -e '
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const report = JSON.parse(fs.readFileSync("/tmp/skymoney-api-audit.json", "utf8"));
|
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}>> {
|
): Promise<Array<{id: string, name: string, dueOn: Date}>> {
|
||||||
// Fixed plans
|
// Fixed plans
|
||||||
const planUpdates = new Map<string, number>();
|
const planUpdates = new Map<string, number>();
|
||||||
|
const fixedAllocationRows = new Map<string, { planId: string; source: FixedAllocation["source"]; amountCents: number }>();
|
||||||
result.fixedAllocations.forEach((a) => {
|
result.fixedAllocations.forEach((a) => {
|
||||||
planUpdates.set(a.fixedPlanId, (planUpdates.get(a.fixedPlanId) ?? 0) + a.amountCents);
|
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}> = [];
|
const fullyFundedPlans: Array<{id: string, name: string, dueOn: Date}> = [];
|
||||||
@@ -834,13 +846,19 @@ async function applyAllocations(
|
|||||||
needsFundingThisPeriod: markFundedThisPeriod ? false : !isFullyFunded,
|
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({
|
await tx.allocation.create({
|
||||||
data: {
|
data: {
|
||||||
userId,
|
userId,
|
||||||
kind: "fixed",
|
kind: "fixed",
|
||||||
toId: planId,
|
toId: row.planId,
|
||||||
amountCents: BigInt(amt),
|
amountCents: BigInt(rowAmount),
|
||||||
incomeId,
|
// 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 { prisma, resetUser, ensureUser, U, cid, pid, closePrisma } from "./helpers";
|
||||||
import { allocateIncome, buildPlanStates } from "../src/allocator";
|
import { allocateIncome, buildPlanStates } from "../src/allocator";
|
||||||
|
|
||||||
|
const DAY_MS = 86_400_000;
|
||||||
|
|
||||||
describe("allocator — new funding system", () => {
|
describe("allocator — new funding system", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await resetUser(U);
|
await resetUser(U);
|
||||||
@@ -228,5 +230,58 @@ describe("allocator — new funding system", () => {
|
|||||||
const sum = variable.reduce((s, a: any) => s + (a.amountCents ?? 0), 0);
|
const sum = variable.reduce((s, a: any) => s + (a.amountCents ?? 0), 0);
|
||||||
expect(sum).toBe(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