mirror of
https://github.com/Sea-Haven-Industries/payments-dashboard.git
synced 2026-09-30 13:33:12 +00:00
Same-day settlement visibility plus maximal data capture from the
reporting feed. The handler gains an allowlisted endpoint parameter
(EventBridge passes {"endpoint":"current-day"} on a new 16/19/22 UTC
weekday rule; the 9am previous-day sweep is unchanged and remains
authoritative). Every response's exact bytes archive to a new
Retain-protected bucket before classification, so the feed is
replayable and auditable even across parser changes. Summary rows
become per-date boa_balance# snapshots (latest-wins on run_at, no
TTL) instead of being discarded. The staleness sweep is gated to
previous-day runs so intraday runs don't re-alert the backlog three
times a day. History events carry a via:<endpoint> audit tag outside
the replay-idempotence identity. Fixtures are sanitized real API
captures; classification histograms assert against live-verified
counts.
Refs: #66, #69
142 lines
5.5 KiB
JavaScript
142 lines
5.5 KiB
JavaScript
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);
|
|
});
|