mirror of
https://github.com/Sea-Haven-Industries/payments-dashboard.git
synced 2026-10-01 19:43:12 +00:00
processPaymentCsv auto-cleared ACH the moment send_payment_on passed, but the bank disagrees often enough to matter: settlement lags the send date by up to 8 days, settled ACH can bounce and re-debit (Uline $19,281.12, Heights $10,000.00 on the 5/26 overdrawn week), and voided ACH that settled anyway returned via electronic credits invisible to a Check-only feed. Move ACH to bank confirmation: - processPaymentCsv: ACH rows keep their Stampli status; the send-date auto-clear is removed. The bankConfirmed protection is unchanged, so bank-cleared records still cannot be moved backward by the CSV. - fetchBoaTransactions scans all payments (method filter dropped) and processes ACH CCD lines: match by stored pmt_id first (reversals reuse the original PMT id), then by the Stampli payment number embedded in PMT INFO (internal spaces stripped, amount must agree), then vendor + exact amount within send_payment_on -2..+14 days. Settled debit -> Cleared/Cleared with dates, pmt_id persisted on the record; credit reusing the pmt_id (or a unique same-amount return credit against a bank-confirmed ACH) -> clear_status=Returned + returned_date + history event. Ambiguity is unmatched, no write. Existing DDB ACH records already Cleared by the old auto-clear are unaffected: bank confirmation is additive and the CSV path never moves a record backward. Handler event contract extended (optional fromDate/toDate); cross-family review required before merge.
535 lines
21 KiB
JavaScript
535 lines
21 KiB
JavaScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import {
|
|
BAI_CODE_EVENTS,
|
|
amountsEqual,
|
|
applyEvent,
|
|
classifyTransaction,
|
|
isCancelStatus,
|
|
isValidISODate,
|
|
matchAchTransaction,
|
|
matchCheckTransaction,
|
|
matchElectronicReturn,
|
|
resolveDateRange,
|
|
vendorMatches,
|
|
} 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 <n>' 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);
|
|
});
|
|
|
|
// ------------------------------------------------------------ ACH (#69)
|
|
|
|
const achPayment = (over = {}) => ({
|
|
pk: `payment#${over.check_number ?? "21222000264"}`,
|
|
check_number: "21222000264",
|
|
method: "ACH",
|
|
payee: "Cobra Septic",
|
|
amount_usd: 8300,
|
|
status: "Payment Submitted",
|
|
send_payment_on: "06/02/2026",
|
|
...over,
|
|
});
|
|
|
|
test("ach matcher: embedded payment number + amount wins", () => {
|
|
const payments = [achPayment(), achPayment({ check_number: "21222000265", pk: "payment#21222000265" })];
|
|
const m = matchAchTransaction(
|
|
{ event: "ach_debit", pmtId: "7584198", embeddedPaymentNumber: "21222000264", vendorText: "Cobra Septic", amount: 8300 },
|
|
payments,
|
|
"2026-06-08"
|
|
);
|
|
assert.equal(m.payment.check_number, "21222000264");
|
|
assert.equal(m.matchedBy, "payment-number+amount");
|
|
});
|
|
|
|
test("ach matcher: embedded number with wrong amount falls through to vendor+amount", () => {
|
|
const payments = [
|
|
achPayment({ check_number: "21222000264", amount_usd: 100 }),
|
|
achPayment({ check_number: "21222000265", pk: "payment#21222000265", amount_usd: 8300 }),
|
|
];
|
|
const m = matchAchTransaction(
|
|
{ event: "ach_debit", pmtId: "7584198", embeddedPaymentNumber: "21222000264", vendorText: "Cobra Septic", amount: 8300 },
|
|
payments,
|
|
"2026-06-08"
|
|
);
|
|
assert.equal(m.payment.check_number, "21222000265");
|
|
assert.equal(m.matchedBy, "vendor+amount");
|
|
});
|
|
|
|
test("ach matcher: stored pmt_id attributes a reversal credit", () => {
|
|
const payments = [
|
|
achPayment({ pmt_id: "7584198", clear_status: "Cleared", status: "Cleared" }),
|
|
];
|
|
const m = matchAchTransaction(
|
|
{ event: "ach_return", pmtId: "7584198", embeddedPaymentNumber: null, vendorText: "Sea Haven Industries, Inc", amount: 8300 },
|
|
payments,
|
|
"2026-06-10"
|
|
);
|
|
assert.equal(m.matchedBy, "pmt_id");
|
|
});
|
|
|
|
test("ach matcher: vendor+amount only within send_payment_on -2..+14 days", () => {
|
|
// Reddi 21222000211: sent 5/27, debited 6/4 (8-day lag) must match...
|
|
const inWindow = [achPayment({ payee: "Reddi Services", send_payment_on: "05/27/2026", amount_usd: 1958 })];
|
|
const m1 = matchAchTransaction(
|
|
{ event: "ach_debit", pmtId: "1", embeddedPaymentNumber: null, vendorText: "Reddi Services", amount: 1958 },
|
|
inWindow,
|
|
"2026-06-04"
|
|
);
|
|
assert.equal(m1.matchedBy, "vendor+amount");
|
|
// ...but a posting 30 days after the send date must not.
|
|
const m2 = matchAchTransaction(
|
|
{ event: "ach_debit", pmtId: "1", embeddedPaymentNumber: null, vendorText: "Reddi Services", amount: 1958 },
|
|
inWindow,
|
|
"2026-06-26"
|
|
);
|
|
assert.equal(m2.payment, undefined);
|
|
});
|
|
|
|
test("ach matcher: same-amount return credit after the debit matches a bank-confirmed ACH", () => {
|
|
const payments = [
|
|
achPayment({ payee: "Uline", clear_status: "Cleared", status: "Cleared", amount_usd: 19281.12 }),
|
|
];
|
|
// Reversal credit: originator is SEA HAVEN INDUST, so vendor match fails.
|
|
const m = matchAchTransaction(
|
|
{ event: "ach_return", pmtId: "999", embeddedPaymentNumber: null, vendorText: "Sea Haven Industries, Inc", amount: 19281.12 },
|
|
payments,
|
|
"2026-05-26"
|
|
);
|
|
assert.equal(m.matchedBy, "amount+cleared");
|
|
});
|
|
|
|
test("ach matcher: ambiguity is unmatched — no write", () => {
|
|
const payments = [
|
|
achPayment({ pk: "payment#a", check_number: "a", clear_status: "Cleared", amount_usd: 500 }),
|
|
achPayment({ pk: "payment#b", check_number: "b", clear_status: "Cleared", amount_usd: 500 }),
|
|
];
|
|
const m = matchAchTransaction(
|
|
{ event: "ach_return", pmtId: "999", embeddedPaymentNumber: null, vendorText: null, amount: 500 },
|
|
payments,
|
|
"2026-06-08"
|
|
);
|
|
assert.equal(m.payment, undefined);
|
|
assert.match(m.unmatched, /ambiguous/);
|
|
});
|
|
|
|
test("vendorMatches: truncated bank originator vs full payee, punctuation-insensitive", () => {
|
|
assert.equal(vendorMatches("Hugill's Septic Service LLC", "Hugill's Septic Service LLC"), true);
|
|
assert.equal(vendorMatches("Alliance Sanitation LLC", "ALLIANCE SANITAT"), true);
|
|
assert.equal(vendorMatches("Cobra Septic", "Reddi Services"), false);
|
|
assert.equal(vendorMatches("", "Cobra Septic"), false);
|
|
});
|
|
|
|
test("ach transitions: settled debit clears both fields and persists pmt_id", () => {
|
|
const p = achPayment();
|
|
const r = applyEvent(
|
|
{ ...p },
|
|
{ event: "ach_debit", pmtId: "7584198", amount: 8300, bankReference: "905512345" },
|
|
"2026-06-04"
|
|
);
|
|
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-06-04");
|
|
assert.equal(r.updates.pmt_id, "7584198");
|
|
});
|
|
|
|
test("ach transitions: return credit then re-debit (bounce and re-settle)", () => {
|
|
const p = achPayment({ status: "Cleared", clear_status: "Cleared", pmt_id: "7500000", history: [] });
|
|
const ret = applyEvent(p, { event: "ach_return", pmtId: "7500000", amount: 8300, bankReference: "r1" }, "2026-05-26");
|
|
assert.equal(ret.kind, "returned");
|
|
assert.equal(ret.updates.clear_status, "Returned");
|
|
assert.equal(ret.updates.returned_date, "2026-05-26");
|
|
Object.assign(p, ret.updates);
|
|
p.history.push(ret.historyEvent);
|
|
|
|
const redebit = applyEvent(p, { event: "ach_debit", pmtId: "7500000", amount: 8300, bankReference: "d2" }, "2026-06-01");
|
|
assert.equal(redebit.kind, "redeposit");
|
|
assert.equal(redebit.updates.status, "Cleared");
|
|
assert.equal(redebit.updates.clear_status, "Cleared");
|
|
});
|
|
|
|
test("ach transitions: electronic return on a voided-but-settled ACH is voided-and-bounced", () => {
|
|
const p = achPayment({ status: "Voided", clear_status: "Cleared" });
|
|
const r = applyEvent(p, { event: "electronic_return", amount: 8300, bankReference: "r2" }, "2026-06-09");
|
|
assert.equal(r.kind, "voided_and_bounced");
|
|
assert.equal(r.updates.status, "Voided");
|
|
assert.equal(r.updates.clear_status, "Returned");
|
|
});
|
|
|
|
// ------------------------------------------------------------- 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);
|
|
});
|