'use strict'; // P1 — computeAmountSuggestionsBatch replaces up to 12 per-bill queries with two // batched queries in getTracker. This pins that the batched form is byte-identical // to calling the single-bill computeAmountSuggestion per bill (the behavior we // rely on for the N+1 fix to be safe). 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-amountsug-${process.pid}.sqlite`); process.env.DB_PATH = dbPath; const { getDb, closeDb } = require('../db/database'); const { computeAmountSuggestion, computeAmountSuggestionsBatch } = require('../services/amountSuggestionService'); let db, userId, bills; test.before(() => { db = getDb(); userId = db.prepare("INSERT INTO users (username, password_hash, role, active) VALUES ('as-user','x','user',1)").run().lastInsertRowid; const insBill = db.prepare("INSERT INTO bills (user_id, name, due_day, expected_amount, active) VALUES (?, ?, 1, 10000, 1)"); bills = { payments: insBill.run(userId, 'Payments only').lastInsertRowid, // suggestion from payment sums mbs: insBill.run(userId, 'Corrected').lastInsertRowid, // user-corrected actual_amount wins mixed: insBill.run(userId, 'Mixed').lastInsertRowid, // some months mbs, some payments empty: insBill.run(userId, 'No history').lastInsertRowid, // → null suggestion }; const pay = db.prepare("INSERT INTO payments (bill_id, amount, paid_date, payment_source) VALUES (?, ?, ?, 'manual')"); // For query month = 2026-06: prior months are May, Apr, Mar, Feb, Jan 2026, Dec 2025. pay.run(bills.payments, 10000, '2026-05-15'); pay.run(bills.payments, 12000, '2026-04-15'); pay.run(bills.payments, 11000, '2026-03-15'); // Two payments in one month should sum. pay.run(bills.mixed, 3000, '2026-05-10'); pay.run(bills.mixed, 2000, '2026-05-20'); // May total 5000 pay.run(bills.mixed, 9000, '2026-04-10'); const mbs = db.prepare("INSERT INTO monthly_bill_state (bill_id, year, month, actual_amount) VALUES (?, ?, ?, ?)"); mbs.run(bills.mbs, 2026, 5, 8000); mbs.run(bills.mbs, 2026, 4, 8500); mbs.run(bills.mbs, 2026, 3, 7500); // Mixed: March has a corrected amount that should win over any payment sum. mbs.run(bills.mixed, 2026, 3, 4200); }); test.after(() => { closeDb(); for (const s of ['', '-wal', '-shm']) { try { fs.rmSync(dbPath + s); } catch {} } }); test('batch output equals per-bill computeAmountSuggestion for every bill', () => { const ids = Object.values(bills); const batch = computeAmountSuggestionsBatch(db, ids, 2026, 6); for (const id of ids) { const single = computeAmountSuggestion(db, id, 2026, 6); assert.deepEqual(batch.get(id), single, `bill ${id} batch matches single`); } }); test('empty bill list returns an empty map; no-history bill → null', () => { assert.equal(computeAmountSuggestionsBatch(db, [], 2026, 6).size, 0); const batch = computeAmountSuggestionsBatch(db, [bills.empty], 2026, 6); assert.equal(batch.get(bills.empty), null); }); test('corrected monthly amount wins over payment sums (median of mbs values)', () => { const s = computeAmountSuggestion(db, bills.mbs, 2026, 6); // median of [8000, 8500, 7500] = 8000 → $80 assert.equal(s.suggestion, 80); assert.equal(s.months_used, 3); assert.equal(s.confidence, 'high'); });