import { test } from "node:test"; import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { classifyTransaction, extractBalances, resolveDateRange, resolveEndpoint, } from "../src/boaRecon.js"; const fixture = (name) => JSON.parse(readFileSync(new URL(`./fixtures/${name}.json`, import.meta.url))) .accountTransactions[0].transactions; const histogram = (txns) => { const h = {}; for (const t of txns) { const e = classifyTransaction(t).event; h[e] = (h[e] || 0) + 1; } return h; }; // --- resolveEndpoint --- test("resolveEndpoint: defaults to previous-day; accepts current-day", () => { assert.equal(resolveEndpoint({}), "previous-day"); assert.equal(resolveEndpoint(undefined), "previous-day"); assert.equal(resolveEndpoint({ endpoint: "current-day" }), "current-day"); }); test("resolveEndpoint: rejects anything else (URL injection guard)", () => { for (const bad of ["previous-day/../admin", "current", "", "PREVIOUS-DAY", 42]) { assert.throws(() => resolveEndpoint({ endpoint: bad }), /endpoint must be one of/); } }); // --- endpoint-aware resolveDateRange --- test("resolveDateRange: current-day default pins today..today", () => { const r = resolveDateRange({}, new Date("2026-07-22T16:00:00Z"), "current-day"); assert.deepEqual(r, { fromDate: "2026-07-22", toDate: "2026-07-22" }); }); test("resolveDateRange: current-day explicit range passes through (API accepts ranges)", () => { const r = resolveDateRange({ fromDate: "2026-07-21", toDate: "2026-07-22" }, new Date(), "current-day"); assert.deepEqual(r, { fromDate: "2026-07-21", toDate: "2026-07-22" }); }); test("resolveDateRange: previous-day default unchanged by endpoint param", () => { const r = resolveDateRange({}, new Date("2026-07-21T13:00:00Z"), "previous-day"); assert.deepEqual(r, { fromDate: "2026-07-14", toDate: "2026-07-20" }); }); // --- extractBalances --- test("extractBalances: current-day fixture yields one snapshot with named fields", () => { const snaps = extractBalances(fixture("current-day")); assert.equal(snaps.length, 1); const s = snaps[0]; assert.equal(s.as_of_date, "2026-07-22"); assert.equal(s.opening_ledger, 209541.48); assert.equal(s.current_ledger, 203890.83); assert.equal(s.opening_available, 209541.48); assert.equal(s.current_available, 203890.83); assert.equal(s.total_credits, 15000); assert.equal(s.total_credits_count, 1); assert.equal(s.total_debits, 20650.65); assert.equal(s.total_debits_count, 5); assert.equal(s.total_ach_debits, 20650.65); // Unrecognized summary codes captured, never dropped. assert.ok(Object.keys(s.other).length > 0); assert.equal(s.other["200"], 15000); }); test("extractBalances: multi-day window groups one snapshot per asOfDate", () => { const snaps = extractBalances(fixture("previous-day-week")); const dates = snaps.map((s) => s.as_of_date).sort(); assert.deepEqual(dates, ["2026-07-15", "2026-07-16", "2026-07-17", "2026-07-20", "2026-07-21"]); for (const s of snaps) assert.ok(typeof s.opening_ledger === "number"); }); test("extractBalances: Detail rows and garbage dates are excluded", () => { const snaps = extractBalances([ { transactionType: "Detail", transactionCode: "010", asOfDate: "2026-07-22", amount: "5" }, { transactionType: "Summary", transactionCode: "010", asOfDate: "garbage", amount: "5" }, ]); assert.deepEqual(snaps, []); }); // --- fixture-driven classification histograms (real captured responses) --- test("fixture: previous-day returns window classifies exactly as the live replay did", () => { const h = histogram(fixture("previous-day-returns")); assert.deepEqual(h, { summary: 100, ignored: 3, check_paid: 32, check_return: 10, electronic_return: 4, ach_debit: 4, }); }); test("fixture: quiet previous-day week has zero unknowns and full summary coverage", () => { const h = histogram(fixture("previous-day-week")); assert.equal(h.unknown ?? 0, 0); assert.equal(h.summary, 250); assert.equal(Object.values(h).reduce((a, b) => a + b, 0), 259); }); test("fixture: current-day classifies the five live ACH settlements", () => { const h = histogram(fixture("current-day")); assert.equal(h.ach_debit, 5); assert.equal(h.unknown ?? 0, 0); assert.equal(h.summary, 46); }); test("fixture: current-day range covers both days without unknowns", () => { const h = histogram(fixture("current-day-range")); assert.equal(h.unknown ?? 0, 0); assert.equal(h.ach_debit, 5); }); test("extractBalances: hostile responses hit caps and finite guards, counted not silent", () => { const rows = []; for (let i = 0; i < 60; i++) rows.push({ transactionType: "Summary", transactionCode: String(500 + i), asOfDate: "2026-07-22", amount: "1" }); rows.push({ transactionType: "Summary", transactionCode: "010", asOfDate: "2026-07-22", amount: "Infinity" }); rows.push({ transactionType: "Summary", transactionCode: "100", asOfDate: "2026-07-22", amount: "5", itemCount: "Infinity" }); for (const month of ["05", "06"]) for (let d = 1; d <= 28; d++) rows.push({ transactionType: "Summary", transactionCode: "030", asOfDate: `2026-${month}-${String(d).padStart(2, "0")}`, amount: "1" }); const snaps = extractBalances(rows); const s = snaps.find((x) => x.as_of_date === "2026-07-22"); assert.ok(Object.keys(s.other).length <= 50); assert.ok(s.other_truncated > 0); assert.equal(s.opening_ledger, undefined); // Infinity amount skipped assert.equal(s.total_credits_count, 0); // Infinity itemCount zeroed assert.ok(snaps.length <= 31); assert.ok(snaps[0].dates_truncated > 0); });