final touches for beta skymoney (at least i think)

This commit is contained in:
2026-01-18 00:00:44 -06:00
parent 4eae966f96
commit f4f0ae5df2
161 changed files with 26016 additions and 1966 deletions

View File

@@ -3,7 +3,7 @@ import { previewAllocation, type FixedPlan, type VariableCategory } from "../src
const cats = (defs: Array<Partial<VariableCategory> & { name: string }>): VariableCategory[] =>
defs.map((d, i) => ({
id: i + 1,
id: String(i + 1),
name: d.name,
percent: d.percent ?? 0,
isSavings: d.isSavings ?? false,
@@ -12,12 +12,13 @@ const cats = (defs: Array<Partial<VariableCategory> & { name: string }>): Variab
const plans = (defs: Array<Partial<FixedPlan> & { name: string; totalCents: number }>): FixedPlan[] =>
defs.map((d, i) => ({
id: i + 1,
id: String(i + 1),
name: d.name,
totalCents: d.totalCents,
fundedCents: d.fundedCents ?? 0,
priority: d.priority ?? 100,
dueOn: d.dueOn ?? new Date().toISOString(),
cycleStart: d.cycleStart ?? new Date().toISOString(),
}));
describe("previewAllocation — basics", () => {

32
web/tests/funding.test.ts Normal file
View File

@@ -0,0 +1,32 @@
import { describe, it, expect } from 'vitest';
import { computeFixedFundingStatus } from '../src/utils/funding';
describe('computeFixedFundingStatus', () => {
it('weekly pay, 3-week window, after 1 week funded one-third -> no funding needed', () => {
const cycleStart = new Date('2025-11-01T00:00:00.000Z');
const now = new Date('2025-11-08T00:00:00.000Z'); // 7 days later => 1 paycheck elapsed
const due = new Date(cycleStart.getTime() + 21 * 24 * 60 * 60 * 1000); // 21 days after start
const total = 30000; // $300.00
const funded = Math.round(total / 3); // after 1 paycheck
const plans = [{ id: 'p1', name: 'Rent', totalCents: total, fundedCents: funded, dueOn: due.toISOString(), cycleStart: cycleStart.toISOString() }];
const res = computeFixedFundingStatus('regular', 'weekly', plans, now, false, 100);
expect(res.needsFunding).toBe(false);
expect(res.plans[0].needsFunding).toBe(false);
});
it('weekly pay, 3-week window, after 1 week underfunded -> needs funding', () => {
const cycleStart = new Date('2025-11-01T00:00:00.000Z');
const now = new Date('2025-11-08T00:00:00.000Z'); // 7 days later
const due = new Date(cycleStart.getTime() + 21 * 24 * 60 * 60 * 1000);
const total = 30000; // $300.00
const funded = Math.round(total / 3) - 500; // under by $5
const plans = [{ id: 'p1', name: 'Rent', totalCents: total, fundedCents: funded, dueOn: due.toISOString(), cycleStart: cycleStart.toISOString() }];
const res = computeFixedFundingStatus('regular', 'weekly', plans, now, false, 100);
expect(res.needsFunding).toBe(true);
expect(res.plans[0].needsFunding).toBe(true);
});
});

View File

@@ -0,0 +1,218 @@
import { describe, it, expect } from "vitest";
import { previewAllocation } from "../src/utils/allocatorPreview";
/**
* Test case: Regular income user with payment plans
* Budget: $2,000 per paycheck (weekly)
* Fixed expenses:
* 1. Rent: $1,200 (due in 4 weeks, payment plan enabled) - should fund $300/week
* 2. Insurance: $400 (due in 2 weeks, payment plan enabled) - should fund $200/week
* Variable categories:
* 1. Groceries: 40%
* 2. Entertainment: 30%
* 3. Savings: 30%
*
* Expected allocation this paycheck:
* - Fixed: $500 ($300 + $200)
* - Variable: $1,500 (remaining after fixed)
* - Groceries: $600 (40% of $1,500)
* - Entertainment: $450 (30% of $1,500)
* - Savings: $450 (30% of $1,500)
*/
describe("OnboardingTracker vs API Allocator", () => {
it("should allocate budget correctly with payment plans", () => {
const budgetCents = 200000; // $2,000
// Today's date for testing
const now = new Date("2025-11-26");
// Fixed plans with payment plans enabled
const fixedPlans = [
{
id: "rent",
name: "Rent",
totalCents: 120000, // $1,200
fundedCents: 0, // Not yet funded
dueOn: "2025-12-24", // 4 weeks from now (28 days)
priority: 1,
cycleStart: "2025-11-26",
},
{
id: "insurance",
name: "Insurance",
totalCents: 40000, // $400
fundedCents: 0, // Not yet funded
dueOn: "2025-12-10", // 2 weeks from now (14 days)
priority: 2,
cycleStart: "2025-11-26",
},
];
// Variable categories
const variableCategories = [
{
id: "groceries",
name: "Groceries",
percent: 40,
balanceCents: 0,
isSavings: false,
priority: 1,
},
{
id: "entertainment",
name: "Entertainment",
percent: 30,
balanceCents: 0,
isSavings: false,
priority: 2,
},
{
id: "savings",
name: "Savings",
percent: 30,
balanceCents: 0,
isSavings: true,
priority: 3,
},
];
// Calculate allocation using previewAllocation
const result = previewAllocation(budgetCents, fixedPlans, variableCategories);
console.log("\n=== ONBOARDING TRACKER TEST CASE ===");
console.log("Budget:", budgetCents / 100, "($2,000)");
console.log("\nFixed Expenses:");
result.fixed.forEach((f) => {
const original = fixedPlans.find((fp) => fp.id === f.id);
const daysUntilDue = Math.ceil(
(new Date(original!.dueOn).getTime() - now.getTime()) / (24 * 60 * 60 * 1000)
);
const weeksUntilDue = Math.ceil(daysUntilDue / 7);
const expectedPerPaycheck = original!.totalCents / weeksUntilDue;
console.log(` ${f.name}: $${f.amountCents / 100} allocated`);
console.log(` Total needed: $${original!.totalCents / 100}`);
console.log(` Due in: ${daysUntilDue} days (${weeksUntilDue} weeks)`);
console.log(` Expected per paycheck: $${expectedPerPaycheck / 100}`);
console.log(` Percentage funded: ${Math.round((f.amountCents / original!.totalCents) * 100)}%`);
});
const totalFixedAllocated = result.fixed.reduce((sum, f) => sum + f.amountCents, 0);
console.log(` Total Fixed Allocated: $${totalFixedAllocated / 100}`);
console.log("\nVariable Categories:");
const totalVariableAllocated = result.variable.reduce((sum, v) => sum + v.amountCents, 0);
console.log(` Total Available: $${totalVariableAllocated / 100}`);
result.variable.forEach((v) => {
const original = variableCategories.find((vc) => vc.id === v.id);
const percentage = original?.percent || 0;
const expectedAmount = Math.floor((totalVariableAllocated * percentage) / 100);
console.log(` ${v.name}: $${v.amountCents / 100} (${percentage}%)`);
console.log(` Expected: $${expectedAmount / 100}`);
});
console.log("\nUnallocated:", result.unallocatedCents / 100);
// Assertions
// 1. Rent should get $300 (1200/4 weeks)
const rentAllocation = result.fixed.find((f) => f.id === "rent");
expect(rentAllocation?.amountCents).toBe(30000); // $300
// 2. Insurance should get $200 (400/2 weeks)
const insuranceAllocation = result.fixed.find((f) => f.id === "insurance");
expect(insuranceAllocation?.amountCents).toBe(20000); // $200
// 3. Total fixed should be $500
expect(totalFixedAllocated).toBe(50000);
// 4. Variable budget should be $1,500 (2000 - 500)
expect(totalVariableAllocated).toBe(150000);
// 5. Groceries should get $600 (40% of $1,500)
const groceriesAllocation = result.variable.find((v) => v.id === "groceries");
expect(groceriesAllocation?.amountCents).toBe(60000); // $600
// 6. Entertainment should get $450 (30% of $1,500)
const entertainmentAllocation = result.variable.find((v) => v.id === "entertainment");
expect(entertainmentAllocation?.amountCents).toBe(45000); // $450
// 7. Savings should get $450 (30% of $1,500)
const savingsAllocation = result.variable.find((v) => v.id === "savings");
expect(savingsAllocation?.amountCents).toBe(45000); // $450
// 8. No unallocated funds
expect(result.unallocatedCents).toBe(0);
console.log("\n✓ All assertions passed!");
});
it("should handle partial funding correctly", () => {
const budgetCents = 100000; // $1,000
// Fixed plan that needs $600 total, already has $300 funded
const fixedPlans = [
{
id: "expense1",
name: "Expense 1",
totalCents: 60000, // $600 total
fundedCents: 30000, // $300 already funded
dueOn: "2025-12-10",
priority: 1,
cycleStart: "2025-11-26",
},
];
// Variable categories
const variableCategories = [
{
id: "cat1",
name: "Category 1",
percent: 50,
balanceCents: 0,
isSavings: false,
priority: 1,
},
{
id: "cat2",
name: "Category 2",
percent: 50,
balanceCents: 0,
isSavings: false,
priority: 2,
},
];
const result = previewAllocation(budgetCents, fixedPlans, variableCategories);
console.log("\n=== PARTIAL FUNDING TEST ===");
console.log("Budget: $1,000");
console.log("Fixed expense needs: $600 total, $300 already funded");
console.log("Should allocate: $300 to fixed, $700 to variable");
const totalFixedAllocated = result.fixed.reduce((sum, f) => sum + f.amountCents, 0);
const totalVariableAllocated = result.variable.reduce((sum, v) => sum + v.amountCents, 0);
console.log("\nFixed allocated:", totalFixedAllocated / 100);
console.log("Variable allocated:", totalVariableAllocated / 100);
// Fixed should only get $300 (remaining need)
expect(totalFixedAllocated).toBe(30000);
// Variable should get $700
expect(totalVariableAllocated).toBe(70000);
// Category 1 should get $350 (50% of $700)
const cat1 = result.variable.find((v) => v.id === "cat1");
expect(cat1?.amountCents).toBe(35000);
// Category 2 should get $350 (50% of $700)
const cat2 = result.variable.find((v) => v.id === "cat2");
expect(cat2?.amountCents).toBe(35000);
console.log("✓ Partial funding test passed!");
});
});