diff --git a/tests/summarySkipOverride.test.js b/tests/summarySkipOverride.test.js new file mode 100644 index 0000000..0a4529a --- /dev/null +++ b/tests/summarySkipOverride.test.js @@ -0,0 +1,72 @@ +'use strict'; + +// B2/B5: the Summary must honour per-month monthly_bill_state modifiers — skipped +// bills excluded from the total, per-month amount overrides applied — alongside the +// occurrence gate added for QA-B5-01. Guards against regressions in either. +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-summary-skip-test-${process.pid}.sqlite`); +process.env.DB_PATH = dbPath; + +const { getDb, closeDb } = require('../db/database'); + +function callSummary(userId, year, month) { + const router = require('../routes/summary'); + const layer = router.stack.find(item => item.route?.path === '/' && item.route.methods.get); + const handler = layer.route.stack[0].handle; + return new Promise((resolve) => { + const req = { query: { year: String(year), month: String(month) }, user: { id: userId, role: 'user' } }; + const res = { + statusCode: 200, + status(c) { this.statusCode = c; return this; }, + json(data) { resolve({ status: this.statusCode, data }); }, + }; + handler(req, res); + }); +} + +test('summary honours skip (exclude) and per-month amount override', async () => { + const db = getDb(); + const userId = db.prepare( + "INSERT INTO users (username, password_hash, role, active) VALUES ('skip-user', 'x', 'user', 1)", + ).run().lastInsertRowid; + + const insertBill = db.prepare(` + INSERT INTO bills (user_id, name, due_day, billing_cycle, cycle_type, expected_amount, active) + VALUES (?, ?, ?, 'monthly', 'monthly', ?, 1) + `); + const a = insertBill.run(userId, 'Bill A', 5, 10000).lastInsertRowid; // $100 + const b = insertBill.run(userId, 'Bill B', 10, 20000).lastInsertRowid; // $200, skipped + const c = insertBill.run(userId, 'Bill C', 15, 30000).lastInsertRowid; // $300, overridden to $50 + + const setState = db.prepare( + 'INSERT INTO monthly_bill_state (bill_id, year, month, actual_amount, is_skipped) VALUES (?, 2026, 6, ?, ?)', + ); + setState.run(b, null, 1); // skip B + setState.run(c, 5000, 0); // override C → $50 + + const { data } = await callSummary(userId, 2026, 6); + const find = (id) => data.expenses.find((e) => e.bill_id === id); + + assert.equal(find(a).display_amount, 100, 'Bill A shows base amount'); + assert.equal(find(b).is_skipped, true, 'Bill B is flagged skipped'); + assert.equal(find(c).display_amount, 50, 'Bill C shows the per-month override'); + assert.equal(find(c).actual_amount, 50, 'Bill C override surfaced as actual_amount'); + + // Counted total excludes the skipped bill and uses the override: 100 + 50 = 150. + const countedTotal = data.expenses + .filter((e) => !e.is_skipped) + .reduce((sum, e) => sum + e.display_amount, 0); + assert.equal(countedTotal, 150, 'monthly obligation excludes skipped, applies override'); +}); + +test.after(() => { + closeDb(); + for (const suffix of ['', '-wal', '-shm']) { + try { fs.rmSync(dbPath + suffix); } catch { /* ignore */ } + } +});