105 lines
3.7 KiB
JavaScript
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);
|
|
});
|