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); });