import { test } from "node:test"; import assert from "node:assert/strict"; import { BAI_CODE_EVENTS, amountsEqual, applyEvent, classifyTransaction, isCancelStatus, isValidISODate, matchCheckTransaction, matchElectronicReturn, resolveDateRange, } from "../src/boaRecon.js"; // Description fixtures are real statement lines from the 2026-07-21 // reconciliation (amounts/refs anonymized where they don't matter). test("classifier: BAI code 475 is a check paid debit", () => { const c = classifyTransaction({ transactionCode: "475", customerReference: "0001122200030", bankReference: "813312345", amount: "1,892.62", }); assert.equal(c.event, "check_paid"); assert.equal(c.direction, "debit"); assert.equal(c.checkNumber, "1122200030"); assert.equal(c.amount, 1892.62); assert.equal(BAI_CODE_EVENTS["475"].event, "check_paid"); }); test("classifier: statement-style 'Check ' description is check paid", () => { const c = classifyTransaction({ text: "Check 3176", amount: "-350" }); assert.equal(c.event, "check_paid"); assert.equal(c.checkNumber, "3176"); }); test("classifier: ARP refer-to-maker return credit", () => { const c = classifyTransaction({ transactionCode: "354", text: "ARP RETURNED CHECK REFER TO MAKER CHECK # 1122200030 PAID DATE 02/19/26", amount: "1,892.62", }); assert.equal(c.event, "check_return"); assert.equal(c.direction, "credit"); assert.equal(c.checkNumber, "1122200030"); }); test("classifier: return of posted check, check-numbered variant", () => { const c = classifyTransaction({ text: "RETURN OF POSTED CHECK / ITEM (RECEIVED ON 03-02) CHECK #1122200236", amount: "3,350.00", }); assert.equal(c.event, "check_return"); assert.equal(c.checkNumber, "1122200236"); }); test("classifier: return of posted check, electronic variant has no check number", () => { const c = classifyTransaction({ text: "RETURN OF POSTED CHECK / ITEM (RECEIVED ON 03-02) ELECTRONIC TRANSACTION", amount: "3,992.15", }); assert.equal(c.event, "electronic_return"); assert.equal(c.checkNumber, null); }); test("classifier: ACH CCD debit without embedded payment number", () => { const c = classifyTransaction({ text: "FISK EXCAVATING DES:PAYMENTS ID:PMT 6814295 INDN:Sea Haven Industries, CO ID:1811679038 CCD PMT INFO:Fisk Excavating Inc", amount: "-650", }); assert.equal(c.event, "ach_debit"); assert.equal(c.pmtId, "6814295"); assert.equal(c.embeddedPaymentNumber, null); assert.equal(c.vendorText, "Fisk Excavating Inc"); }); test("classifier: ACH debit with embedded payment number", () => { const c = classifyTransaction({ text: "COBRA SEPTIC DES:PAYMENTS ID:PMT 7584198 INDN:Sea Haven Industries, CO ID:1811679038 CCD PMT INFO:Cobra Septic 21222000264", amount: "-8,300.00", }); assert.equal(c.event, "ach_debit"); assert.equal(c.pmtId, "7584198"); assert.equal(c.embeddedPaymentNumber, "21222000264"); assert.equal(c.vendorText, "Cobra Septic"); }); test("classifier: embedded payment number with internal space '21 222000273'", () => { const c = classifyTransaction({ text: "HALL PUMP SALES DES:PAYMENTS ID:PMT 7617407 INDN:Sea Haven Industries, CO ID:1811679038 CCD PMT INFO:Hall Pump Sales & Service Corporation 21 222000273", amount: "-5,495.00", }); assert.equal(c.embeddedPaymentNumber, "21222000273"); assert.equal(c.vendorText, "Hall Pump Sales & Service Corporation"); }); test("classifier: embedded payment number with internal space '2122200 0256'", () => { const c = classifyTransaction({ text: "STERLING SEPTIC DES:PAYMENTS ID:PMT 7603631 INDN:Sea Haven Industries, CO ID:1811679038 CCD PMT INFO:Sterling Septic & Plumbing, LLC. 2122200 0256", amount: "-1,407.59", }); assert.equal(c.embeddedPaymentNumber, "21222000256"); assert.equal(c.vendorText, "Sterling Septic & Plumbing, LLC."); }); test("classifier: ACH reversal credit reusing PMT id is ach_return", () => { const c = classifyTransaction({ text: "SEA HAVEN INDUST DES:PAYMENTS ID:PMT 7176466 INDN:Sea Haven Industries, CO ID:1811679038 CCD PMT INFO:Sea Haven Industries, Inc 21222000128", amount: "2,100.00", }); assert.equal(c.event, "ach_return"); assert.equal(c.pmtId, "7176466"); assert.equal(c.embeddedPaymentNumber, "21222000128"); }); test("classifier: short trailing digits are not an embedded payment number", () => { const c = classifyTransaction({ text: "SOME VENDOR DES:PAYMENTS ID:PMT 700001 INDN:Sea Haven Industries, CO ID:1811679038 CCD PMT INFO:Vendor Company 2000", amount: "-100.00", }); assert.equal(c.embeddedPaymentNumber, null); assert.equal(c.vendorText, "Vendor Company 2000"); }); test("classifier: unknown code on a check-shaped transaction is 'unknown', never dropped", () => { const c = classifyTransaction({ transactionCode: "699", customerReference: "3301", text: "SOMETHING NEW", amount: "-100.00", }); assert.equal(c.event, "unknown"); assert.equal(c.checkShaped, true); assert.equal(c.code, "699"); }); test("classifier: transfers and misc bank activity are ignored", () => { const c = classifyTransaction({ text: "ACCOUNT TRANSFER TRSF FROM 483096772516", amount: "15,000.00", }); assert.equal(c.event, "ignored"); // Not check-shaped (no code, no reference, no CHECK text) -> ignored too. const c2 = classifyTransaction({ text: "RETURN ITEM CHARGEBACK", amount: "-215" }); assert.equal(c2.event, "ignored"); }); test("amountsEqual: comma-tolerant, sign-insensitive, zero never matches", () => { assert.equal(amountsEqual("1,892.62", -1892.62), true); assert.equal(amountsEqual(0, 0), false); assert.equal(amountsEqual("", ""), false); }); // ---------------------------------------------------------------- matcher const payment = (over = {}) => ({ pk: `payment#${over.check_number ?? "1001"}`, check_number: "1001", method: "Check", amount_usd: 500, status: "Outstanding", send_payment_on: "07/01/2026", ...over, }); test("matcher: check number AND amount must both match", () => { const payments = [payment({ check_number: "1001", amount_usd: 500 })]; const m = matchCheckTransaction({ checkNumber: "1001", amount: 500 }, payments, "2026-07-20"); assert.equal(m.payment, payments[0]); assert.equal(m.matchedBy, "number+amount"); }); test("matcher: number match with wrong amount falls back to unique exact-amount match", () => { const payments = [ payment({ check_number: "1001", amount_usd: 500 }), payment({ check_number: "1002", amount_usd: 750.25, pk: "payment#1002" }), ]; // Posting collapsed onto 1001's number but carries 1002's amount. const m = matchCheckTransaction({ checkNumber: "1001", amount: 750.25 }, payments, "2026-07-20"); assert.equal(m.payment.check_number, "1002"); assert.equal(m.matchedBy, "amount"); }); test("matcher: collapsed number we never issued still recovers by amount", () => { const payments = [payment({ check_number: "11222000123", amount_usd: 6413 })]; const m = matchCheckTransaction({ checkNumber: "1222000012", amount: 6413 }, payments, "2026-07-20"); assert.equal(m.payment.check_number, "11222000123"); assert.equal(m.matchedBy, "amount"); }); test("matcher: ambiguous amount fallback is unmatched — no write", () => { const payments = [ payment({ check_number: "1001", amount_usd: 500 }), payment({ check_number: "1002", amount_usd: 350, pk: "payment#1002" }), payment({ check_number: "1003", amount_usd: 350, pk: "payment#1003" }), ]; const m = matchCheckTransaction({ checkNumber: "1001", amount: 350 }, payments, "2026-07-20"); assert.equal(m.payment, undefined); assert.match(m.unmatched, /ambiguous/); }); test("matcher: zero candidates is unmatched", () => { const m = matchCheckTransaction({ checkNumber: "9999", amount: 123.45 }, [payment()], "2026-07-20"); assert.equal(m.payment, undefined); }); test("matcher: amount fallback only considers checks issued in the last 120 days", () => { const payments = [ payment({ check_number: "1001", amount_usd: 350, send_payment_on: "01/02/2026" }), ]; const m = matchCheckTransaction({ checkNumber: "2001", amount: 350 }, payments, "2026-07-20"); assert.equal(m.payment, undefined); }); test("matcher: non-Check payments are never check-match candidates", () => { const payments = [payment({ method: "ACH", check_number: "21222000264", amount_usd: 350 })]; const m = matchCheckTransaction({ checkNumber: "21222000264", amount: 350 }, payments, "2026-07-20"); assert.equal(m.payment, undefined); }); test("electronic return: unique amount among bank-confirmed payments matches", () => { const payments = [ payment({ check_number: "1001", amount_usd: 3992.15, clear_status: "Cleared" }), payment({ check_number: "1002", amount_usd: 3992.15, pk: "payment#1002" }), // not bank-confirmed ]; const m = matchElectronicReturn({ amount: 3992.15 }, payments, "2026-07-20"); assert.equal(m.payment.check_number, "1001"); }); test("electronic return: ambiguity is unmatched", () => { const payments = [ payment({ check_number: "1001", amount_usd: 2500, clear_status: "Cleared" }), payment({ check_number: "1002", amount_usd: 2500, clear_status: "Cleared", pk: "payment#1002" }), ]; const m = matchElectronicReturn({ amount: 2500 }, payments, "2026-07-20"); assert.equal(m.payment, undefined); assert.match(m.unmatched, /ambiguous/); }); // ------------------------------------------------------------ transitions const paidEvent = (over = {}) => ({ event: "check_paid", amount: 500, bankReference: "813300001", ...over, }); const returnEvent = (over = {}) => ({ event: "check_return", amount: 500, bankReference: "813300002", ...over, }); test("transitions: paid debit sets BOTH status and clear_status to Cleared with dates", () => { const p = payment(); const r = applyEvent(p, paidEvent(), "2026-07-19"); assert.equal(r.kind, "cleared"); assert.equal(r.updates.status, "Cleared"); assert.equal(r.updates.clear_status, "Cleared"); assert.equal(r.updates.paid_date, "2026-07-19"); assert.equal(r.updates.cleared_date, "2026-07-19"); assert.equal(r.updates.bank_reference, "813300001"); assert.deepEqual(r.historyEvent, { event: "check_paid", date: "2026-07-19", bankRef: "813300001", amount: 500, }); }); test("transitions: return applies EVEN IF currently Cleared; clear_status is bank truth", () => { const p = payment({ status: "Cleared", clear_status: "Cleared" }); const r = applyEvent(p, returnEvent(), "2026-07-20"); assert.equal(r.kind, "returned"); assert.equal(r.updates.clear_status, "Returned"); assert.equal(r.updates.returned_date, "2026-07-20"); // status has no "Returned" rung on the CSV ladder — re-written unchanged. assert.equal(r.updates.status, "Cleared"); }); test("transitions: second paid debit on a Returned check is a redeposit back to Cleared", () => { const p = payment({ status: "Cleared", clear_status: "Returned" }); const r = applyEvent(p, paidEvent({ bankReference: "813300003" }), "2026-07-21"); assert.equal(r.kind, "redeposit"); assert.equal(r.updates.status, "Cleared"); assert.equal(r.updates.clear_status, "Cleared"); assert.equal(r.updates.paid_date, "2026-07-21"); }); test("transitions: paid -> returned -> redeposit sequence accumulates history", () => { const p = payment(); p.history = p.history || []; const seq = [ [paidEvent(), "2026-07-18"], [returnEvent(), "2026-07-19"], [paidEvent({ bankReference: "813300004" }), "2026-07-20"], ]; const kinds = []; for (const [ev, date] of seq) { const r = applyEvent(p, ev, date); kinds.push(r.kind); Object.assign(p, r.updates); p.history.push(r.historyEvent); } assert.deepEqual(kinds, ["cleared", "returned", "redeposit"]); assert.equal(p.clear_status, "Cleared"); assert.equal(p.history.length, 3); assert.deepEqual( p.history.map((h) => h.event), ["check_paid", "check_return", "check_paid"] ); }); test("transitions: return on a canceled record is terminal voided-and-bounced", () => { const p = payment({ status: "Voided", clear_status: "Cleared" }); const r = applyEvent(p, returnEvent(), "2026-07-20"); assert.equal(r.kind, "voided_and_bounced"); assert.equal(r.updates.clear_status, "Returned"); assert.equal(r.updates.status, "Voided"); // cancel status stays visible }); test("transitions: paid debit on a canceled record keeps the cancel status", () => { const p = payment({ status: "Marked as Void" }); const r = applyEvent(p, paidEvent(), "2026-07-19"); assert.equal(r.kind, "cleared_on_canceled"); assert.equal(r.updates.status, "Marked as Void"); assert.equal(r.updates.clear_status, "Cleared"); }); test("transitions: identical replayed event is a noop (idempotent replays)", () => { const p = payment({ status: "Cleared", clear_status: "Cleared", history: [{ event: "check_paid", date: "2026-07-19", bankRef: "813300001", amount: 500 }], }); const r = applyEvent(p, paidEvent(), "2026-07-19"); assert.equal(r.kind, "noop"); assert.equal(r.updates, null); }); test("transitions: every non-noop result sets both status and clear_status", () => { const cases = [ [payment(), paidEvent()], [payment({ status: "Cleared", clear_status: "Cleared" }), returnEvent()], [payment({ status: "Cleared", clear_status: "Returned" }), paidEvent()], [payment({ status: "Voided" }), returnEvent()], [payment({ clear_status: "Cleared" }), { event: "electronic_return", amount: 500, bankReference: "x" }], ]; for (const [p, ev] of cases) { const r = applyEvent(p, ev, "2026-07-20"); assert.notEqual(r.kind, "noop"); assert.ok("status" in r.updates, `${r.kind} must set status`); assert.ok("clear_status" in r.updates, `${r.kind} must set clear_status`); } }); test("isCancelStatus covers the Stampli cancel vocabulary", () => { for (const s of ["Voided", "cancelled", "Canceled", "Marked as Void"]) { assert.equal(isCancelStatus(s), true); } assert.equal(isCancelStatus("Cleared"), false); }); // ------------------------------------------------------------- date range test("resolveDateRange: default is yesterday", () => { const r = resolveDateRange({}, new Date("2026-07-21T13:00:00Z")); assert.deepEqual(r, { fromDate: "2026-07-20", toDate: "2026-07-20" }); }); test("resolveDateRange: explicit valid range passes through", () => { const r = resolveDateRange({ fromDate: "2026-06-16", toDate: "2026-06-23" }); assert.deepEqual(r, { fromDate: "2026-06-16", toDate: "2026-06-23" }); }); test("resolveDateRange: fromDate only replays a single day", () => { const r = resolveDateRange({ fromDate: "2026-06-08" }); assert.deepEqual(r, { fromDate: "2026-06-08", toDate: "2026-06-08" }); }); test("resolveDateRange: malformed and impossible dates throw", () => { assert.throws(() => resolveDateRange({ fromDate: "06/08/2026" })); assert.throws(() => resolveDateRange({ fromDate: "2026-02-30" })); assert.throws(() => resolveDateRange({ fromDate: "2026-07-02", toDate: "2026-07-01" })); assert.throws(() => resolveDateRange({ fromDate: "2026-07-01; DROP", toDate: "2026-07-02" })); }); test("isValidISODate rejects non-strings and bad calendar dates", () => { assert.equal(isValidISODate("2026-07-20"), true); assert.equal(isValidISODate("2026-13-01"), false); assert.equal(isValidISODate(20260720), false); });