'use strict'; // T1 — the Tracker's core aggregation (getTracker summary + bank tracking + // getOverdueCount) had no dedicated tests despite being the densest money-math // in the app. Covers the occurrence-gating fix (annual/off-month bills must not // inflate the bank card's unpaid/remaining), summary totals, the bank-mode // remaining agreement, cents↔dollars integrity, and overdue gating. const test = require('node:test'); const assert = require('node:assert/strict'); const os = require('node:os'); const path = require('node:path'); const fs = require('node:fs'); const dbPath = path.join(os.tmpdir(), `bill-tracker-trackersvc-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; const { getDb, closeDb } = require('../db/database'); const { getTracker, getOverdueCount } = require('../services/trackerService'); // Fixed "now" so occurrence gating + statuses are deterministic (mid-June 2026). const JUNE_20 = new Date(2026, 5, 20, 12, 0, 0); let db; function mkUser(name) { return db.prepare( "INSERT INTO users (username, password_hash, role, active) VALUES (?, 'x', 'user', 1)", ).run(name).lastInsertRowid; } const insertBill = () => db.prepare(` INSERT INTO bills (user_id, name, due_day, billing_cycle, cycle_type, cycle_day, expected_amount, active) VALUES (?, ?, ?, ?, ?, ?, ?, 1) `); function enableBank(userId, balanceCents) { const acctId = db.prepare(` INSERT INTO financial_accounts (user_id, name, org_name, account_type, balance, available_balance, monitored) VALUES (?, 'Checking', 'Test Bank', 'checking', ?, ?, 1) `).run(userId, balanceCents, balanceCents).lastInsertRowid; const setSetting = db.prepare('INSERT INTO user_settings (user_id, key, value) VALUES (?, ?, ?)'); setSetting.run(userId, 'bank_tracking_enabled', 'true'); setSetting.run(userId, 'bank_tracking_account_id', String(acctId)); // Pending window 0 so no recent-manual-payment guessing affects the balance. setSetting.run(userId, 'bank_tracking_pending_days', '0'); } test.before(() => { db = getDb(); }); test.after(() => { closeDb(); for (const s of ['', '-wal', '-shm']) { try { fs.rmSync(dbPath + s); } catch {} } }); test('bank mode: annual/off-month bill does NOT inflate unpaid_this_month or remaining (gating fix)', () => { const userId = mkUser('t1-gating'); const ins = insertBill(); ins.run(userId, 'Monthly', 15, 'monthly', 'monthly', null, 10000); // $100 due every month ins.run(userId, 'Annual (Jan)', 1, 'annually', 'annual', '1', 50000); // $500, not due in June enableBank(userId, 200000); // $2,000 balance const t = getTracker(userId, { year: 2026, month: 6 }, JUNE_20); assert.equal(t.bank_tracking.enabled, true); // Only the $100 monthly bill is due in June; the $500 annual bill is excluded. assert.equal(t.bank_tracking.unpaid_this_month, 100, 'gated: $100, not $600'); assert.equal(t.bank_tracking.balance, 2000, 'balance in dollars (fromCents)'); assert.equal(t.bank_tracking.remaining, 1900, '2000 − 100 (not 2000 − 600)'); // Fix #2: the summary remaining figures agree with the bank card. assert.equal(t.summary.remaining, 1900); assert.equal(t.summary.total_remaining, 1900); }); test('summary totals: gated expected, total_paid, paid_toward_due (cents→dollars)', () => { const userId = mkUser('t1-summary'); const ins = insertBill(); const billA = ins.run(userId, 'A due 15', 15, 'monthly', 'monthly', null, 10000).lastInsertRowid; // $100 const billB = ins.run(userId, 'B due 1', 1, 'monthly', 'monthly', null, 20000).lastInsertRowid; // $200 ins.run(userId, 'Annual (Jan)', 1, 'annually', 'annual', '1', 90000); // $900, excluded in June const pay = db.prepare( "INSERT INTO payments (bill_id, amount, paid_date, payment_source) VALUES (?, ?, ?, 'manual')", ); pay.run(billA, 10000, '2026-06-15'); // pay A in full ($100) pay.run(billB, 5000, '2026-06-10'); // partial toward B ($50) const t = getTracker(userId, { year: 2026, month: 6 }, JUNE_20); assert.equal(t.summary.total_expected, 300, 'only June bills: 100 + 200 (annual excluded)'); assert.equal(t.summary.total_paid, 150, '100 + 50'); assert.equal(t.summary.paid_toward_due, 150, 'both payments are ≤ their due amount'); assert.equal(t.rows.filter(r => r.name === 'Annual (Jan)').length, 0, 'annual bill absent from rows'); }); test('summary.remaining falls back to outstanding balance when no bank + no starting amounts', () => { const userId = mkUser('t1-nobank'); insertBill().run(userId, 'Solo due 15', 15, 'monthly', 'monthly', null, 10000); // $100, unpaid const t = getTracker(userId, { year: 2026, month: 6 }, JUNE_20); assert.equal(t.bank_tracking.enabled, false); assert.equal(t.summary.has_starting_amounts, false); // No bank / no starting amounts → remaining is the outstanding still-owed for // the current period (June 20 → "15th" period); the $100 bill is unpaid. assert.equal(t.summary.remaining, 100); }); test('getOverdueCount gates by occurrence and honors paid/skip', () => { const userId = mkUser('t1-overdue'); const ins = insertBill(); ins.run(userId, 'Overdue monthly', 1, 'monthly', 'monthly', null, 10000); // due June 1, unpaid → overdue on June 20 ins.run(userId, 'Annual (Jan)', 1, 'annually', 'annual', '1', 50000); // not due in June → NOT overdue const paidBill = ins.run(userId, 'Paid monthly', 1, 'monthly', 'monthly', null, 10000).lastInsertRowid; db.prepare("INSERT INTO payments (bill_id, amount, paid_date, payment_source) VALUES (?, 10000, '2026-06-02', 'manual')") .run(paidBill); const { count, names } = getOverdueCount(userId, JUNE_20); assert.equal(count, 1, 'only the unpaid monthly bill due June 1 is overdue'); assert.deepEqual(names, ['Overdue monthly']); });