BillTracker/tests/safeToSpend.test.js

105 lines
3.7 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const { buildSafeToSpend } = require('../services/trackerService');
function row(overrides = {}) {
return {
id: 1,
name: 'Bill',
due_date: '2026-06-12',
status: 'upcoming',
balance: 50,
...overrides,
};
}
test('safe to spend subtracts bills still due before the next payday', () => {
const result = buildSafeToSpend({
activeRows: [
row({ id: 1, name: 'Rent', due_date: '2026-06-12', balance: 800 }),
row({ id: 2, name: 'Power', due_date: '2026-06-14', balance: 120.5 }),
row({ id: 3, name: 'Paid one', due_date: '2026-06-13', status: 'paid', balance: 0 }),
row({ id: 4, name: 'After payday', due_date: '2026-06-20', balance: 60 }),
],
available: 1500,
todayStr: '2026-06-10',
year: 2026,
month: 6,
dayOfMonth: 10,
});
assert.equal(result.next_payday, '2026-06-15');
assert.equal(result.days_until_payday, 5);
assert.equal(result.still_due_count, 2);
assert.equal(result.still_due_total, 920.5);
assert.equal(result.safe_to_spend, 579.5);
assert.deepEqual(result.upcoming.map(u => u.name), ['Rent', 'Power']);
});
test('second half of the month rolls payday to the 1st, December wraps the year', () => {
const june = buildSafeToSpend({
activeRows: [], available: 100, todayStr: '2026-06-20', year: 2026, month: 6, dayOfMonth: 20,
});
assert.equal(june.next_payday, '2026-07-01');
const dec = buildSafeToSpend({
activeRows: [], available: 100, todayStr: '2026-12-20', year: 2026, month: 12, dayOfMonth: 20,
});
assert.equal(dec.next_payday, '2027-01-01');
assert.equal(dec.days_until_payday, 12);
});
test('overdue bills count against safe-to-spend and land on today in the timeline', () => {
const result = buildSafeToSpend({
activeRows: [
row({ id: 1, name: 'Late card', due_date: '2026-06-03', status: 'late', balance: 200 }),
row({ id: 2, name: 'Internet', due_date: '2026-06-13', balance: 80 }),
],
available: 500,
todayStr: '2026-06-10',
year: 2026,
month: 6,
dayOfMonth: 10,
});
assert.equal(result.safe_to_spend, 220);
// Overdue bill is charged on today's entry, not in the past
const todayEntry = result.timeline.find(t => t.date === '2026-06-10');
assert.equal(todayEntry.balance, 300);
assert.deepEqual(todayEntry.bills.map(b => b.name), ['Late card']);
// Timeline ends on payday
assert.equal(result.timeline[result.timeline.length - 1].date, '2026-06-15');
assert.equal(result.timeline[result.timeline.length - 1].balance, 220);
});
test('cent-exact math: no float drift across many bills', () => {
const bills = Array.from({ length: 30 }, (_, i) =>
row({ id: i, name: `B${i}`, due_date: '2026-06-12', balance: 0.1 }));
const result = buildSafeToSpend({
activeRows: bills, available: 10, todayStr: '2026-06-10', year: 2026, month: 6, dayOfMonth: 10,
});
assert.equal(result.still_due_total, 3);
assert.equal(result.safe_to_spend, 7);
});
test('negative safe-to-spend is reported, not clamped', () => {
const result = buildSafeToSpend({
activeRows: [row({ balance: 300 })],
available: 100, todayStr: '2026-06-10', year: 2026, month: 6, dayOfMonth: 10,
});
assert.equal(result.safe_to_spend, -200);
});
test('skipped/zero-balance rows and empty data produce a flat timeline', () => {
const result = buildSafeToSpend({
activeRows: [row({ status: 'paid', balance: 0 })],
available: 250, todayStr: '2026-06-10', year: 2026, month: 6, dayOfMonth: 10,
});
assert.equal(result.still_due_count, 0);
assert.equal(result.safe_to_spend, 250);
assert.equal(result.timeline[0].date, '2026-06-10');
assert.equal(result.timeline[0].balance, 250);
assert.ok(result.timeline[result.timeline.length - 1].payday);
});