mirror of
https://github.com/Sea-Haven-Industries/payments-dashboard.git
synced 2026-09-30 04:13:12 +00:00
- Null-guard current_ledger on the authoritative cash-position tile so a missing ledger renders 'Not available' instead of the false '/bin/bash.00' - Log GetCommand failures with console.error + logSafe instead of silent catch, matching the rest of the codebase - Derive dates from local midnight instead of toISOString UTC, so the current-day lookup doesn't miss from 8pm ET to midnight - Fetch all 14 balance keys in parallel with Promise.allSettled instead of 14 serial GetCommands; start previous-day walk at i=1 since today's previous-day snapshot can't exist until tomorrow - Use Item.as_of_date directly instead of a synthetic _asOfDate field - Remove _asOfDate from test fixtures; add test for the null-ledger guard
211 lines
7.6 KiB
JavaScript
211 lines
7.6 KiB
JavaScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { categorizePayments, buildHomeView } from "../src/slackAppHome.js";
|
|
import { parseISOLocal } from "../src/dates.js";
|
|
|
|
// Fixture shapes mirror real DDB payment records after the fetchBoa v2
|
|
// transitions (#66/#69): a bank-returned check keeps status "Cleared" (the
|
|
// CSV ladder has no Returned rung) while clear_status flips to "Returned".
|
|
|
|
const payment = (overrides = {}) => ({
|
|
method: "Check",
|
|
status: "Sent for payment",
|
|
send_payment_on: "01/05/2020",
|
|
amount_usd: 100,
|
|
payee: "Vendor",
|
|
check_number: "1122200030",
|
|
...overrides,
|
|
});
|
|
|
|
const returnedCheck = (overrides = {}) =>
|
|
payment({
|
|
status: "Cleared",
|
|
clear_status: "Returned",
|
|
returned_date: "2026-02-20",
|
|
...overrides,
|
|
});
|
|
|
|
test("categorize: returned check surfaces despite status Cleared", () => {
|
|
const { returnedPayments, outstandingChecks, scheduledChecks } = categorizePayments([
|
|
returnedCheck(),
|
|
]);
|
|
assert.equal(returnedPayments.length, 1);
|
|
assert.equal(outstandingChecks.length, 0);
|
|
assert.equal(scheduledChecks.length, 0);
|
|
});
|
|
|
|
test("categorize: returned records are excluded from Outstanding totals", () => {
|
|
const { outstandingChecks, bucketedOutstanding, returnedPayments } = categorizePayments([
|
|
returnedCheck({ amount_usd: 5256.62 }),
|
|
payment({ amount_usd: 40 }),
|
|
]);
|
|
assert.equal(outstandingChecks.length, 1);
|
|
assert.equal(returnedPayments.length, 1);
|
|
const bucketTotal = bucketedOutstanding.reduce((sum, b) => sum + b.total, 0);
|
|
assert.equal(bucketTotal, 40);
|
|
});
|
|
|
|
test("categorize: terminal voided-and-bounced stays out of the action queue", () => {
|
|
for (const status of ["Marked as Void", "Voided", "Cancelled", "canceled"]) {
|
|
const { returnedPayments, outstandingChecks } = categorizePayments([
|
|
returnedCheck({ status }),
|
|
]);
|
|
assert.equal(returnedPayments.length, 0, `status ${status} in returned bucket`);
|
|
assert.equal(outstandingChecks.length, 0, `status ${status} in outstanding`);
|
|
}
|
|
});
|
|
|
|
test("categorize: redeposited check (re-Cleared, returned_date retained) is not Returned", () => {
|
|
const { returnedPayments, outstandingChecks } = categorizePayments([
|
|
returnedCheck({ clear_status: "Cleared" }),
|
|
]);
|
|
assert.equal(returnedPayments.length, 0);
|
|
assert.equal(outstandingChecks.length, 0); // status Cleared → skip-list
|
|
});
|
|
|
|
test("categorize: returned ACH is included in the returned bucket", () => {
|
|
const { returnedPayments } = categorizePayments([
|
|
returnedCheck({ method: "ACH", check_number: "PMT123" }),
|
|
]);
|
|
assert.equal(returnedPayments.length, 1);
|
|
});
|
|
|
|
test("categorize: returned surfaces even when send_payment_on no longer parses", () => {
|
|
const { returnedPayments } = categorizePayments([
|
|
returnedCheck({ send_payment_on: "garbage" }),
|
|
]);
|
|
assert.equal(returnedPayments.length, 1);
|
|
});
|
|
|
|
test("categorize: returned bucket sorts oldest return first, unknown date first", () => {
|
|
const { returnedPayments } = categorizePayments([
|
|
returnedCheck({ payee: "B", returned_date: "2026-04-08" }),
|
|
returnedCheck({ payee: "C", returned_date: undefined }),
|
|
returnedCheck({ payee: "A", returned_date: "2026-02-20" }),
|
|
]);
|
|
assert.deepEqual(
|
|
returnedPayments.map((p) => p.payee),
|
|
["C", "A", "B"]
|
|
);
|
|
});
|
|
|
|
test("categorize: non-returned flow is unchanged", () => {
|
|
const { outstandingChecks, scheduledChecks } = categorizePayments([
|
|
payment(),
|
|
payment({ send_payment_on: "12/31/2099" }),
|
|
payment({ status: "Marked as Void" }),
|
|
]);
|
|
assert.equal(outstandingChecks.length, 1);
|
|
assert.equal(scheduledChecks.length, 1);
|
|
});
|
|
|
|
const metadata = { last_updated: new Date().toISOString(), file_name: "export.csv" };
|
|
|
|
test("home view: renders the needs-action section and summary tile when returns exist", () => {
|
|
const view = buildHomeView([returnedCheck({ amount_usd: 1892.62 }), payment()], metadata);
|
|
const json = JSON.stringify(view);
|
|
assert.ok(json.includes("Returned — Needs Action"));
|
|
assert.ok(json.includes("reissue or void decision"));
|
|
assert.ok(json.includes(":rotating_light: *Returned*"));
|
|
assert.ok(json.includes("$1,892.62"));
|
|
});
|
|
|
|
test("home view: omits the returned section and tile when there are none", () => {
|
|
const view = buildHomeView([payment()], metadata);
|
|
const json = JSON.stringify(view);
|
|
assert.ok(!json.includes("Returned — Needs Action"));
|
|
assert.ok(!json.includes(":rotating_light: *Returned*"));
|
|
});
|
|
|
|
test("home view: unknown returned_date renders without an age instead of crashing", () => {
|
|
const view = buildHomeView([returnedCheck({ returned_date: "not-a-date" })], metadata);
|
|
assert.ok(JSON.stringify(view).includes("Return date unknown"));
|
|
});
|
|
|
|
test("parseISOLocal: valid ISO date parses to local midnight", () => {
|
|
const dt = parseISOLocal("2026-02-20");
|
|
assert.equal(dt.getFullYear(), 2026);
|
|
assert.equal(dt.getMonth(), 1);
|
|
assert.equal(dt.getDate(), 20);
|
|
assert.equal(dt.getHours(), 0);
|
|
});
|
|
|
|
const balanceItem = (overrides = {}) => ({
|
|
as_of_date: "2026-07-22",
|
|
endpoint: "previous-day",
|
|
opening_ledger: 209541.48,
|
|
current_ledger: 203890.83,
|
|
opening_available: 209541.48,
|
|
current_available: 203890.83,
|
|
float_one_day: 0,
|
|
total_credits: 15000,
|
|
total_credits_count: 1,
|
|
total_debits: 20650.65,
|
|
total_debits_count: 5,
|
|
total_ach_debits: 20650.65,
|
|
other: {},
|
|
...overrides,
|
|
});
|
|
|
|
const intradayItem = (overrides = {}) => ({
|
|
as_of_date: "2026-07-22",
|
|
endpoint: "current-day",
|
|
opening_ledger: 203890.83,
|
|
current_ledger: 199500.12,
|
|
opening_available: 203890.83,
|
|
current_available: 199500.12,
|
|
...overrides,
|
|
});
|
|
|
|
test("home view: cash position tile renders when previous-day balance exists", () => {
|
|
const view = buildHomeView([payment()], metadata, [], { previousDay: balanceItem() });
|
|
const json = JSON.stringify(view);
|
|
assert.ok(json.includes(":moneybag: *Cash Position*"));
|
|
assert.ok(json.includes("2026-07-22"));
|
|
assert.ok(json.includes("$203,890.83"));
|
|
});
|
|
|
|
test("home view: cash position shows 'Not available' when balance is missing", () => {
|
|
const view = buildHomeView([payment()], metadata, [], { previousDay: null, intraday: null });
|
|
const json = JSON.stringify(view);
|
|
assert.ok(json.includes("Cash Position"));
|
|
assert.ok(json.includes("Not available"));
|
|
});
|
|
|
|
test("home view: cash position shows 'Not available' when previousDay exists but current_ledger is null", () => {
|
|
const view = buildHomeView([payment()], metadata, [], {
|
|
previousDay: balanceItem({ current_ledger: undefined, current_available: undefined }),
|
|
intraday: null,
|
|
});
|
|
const json = JSON.stringify(view);
|
|
// The cash-position tile falls back to "Not available" instead of "Ledger $0.00"
|
|
assert.ok(json.includes("Not available"));
|
|
// Confirm the tile does NOT contain the false "Ledger" line
|
|
assert.ok(!json.includes("Ledger "));
|
|
});
|
|
|
|
test("home view: intraday provisional tile renders when current-day exists", () => {
|
|
const view = buildHomeView([payment()], metadata, [], {
|
|
previousDay: balanceItem(),
|
|
intraday: intradayItem(),
|
|
});
|
|
const json = JSON.stringify(view);
|
|
assert.ok(json.includes("Intraday (provisional)"));
|
|
assert.ok(json.includes("$199,500.12"));
|
|
});
|
|
|
|
test("home view: intraday tile shows 'not yet available' when balance exists but no intraday", () => {
|
|
const view = buildHomeView([payment()], metadata, [], {
|
|
previousDay: balanceItem(),
|
|
intraday: null,
|
|
});
|
|
const json = JSON.stringify(view);
|
|
assert.ok(json.includes("Intraday not yet available"));
|
|
});
|
|
|
|
test("parseISOLocal: rejects impossible, non-ISO, and empty values", () => {
|
|
assert.equal(parseISOLocal("2026-02-30"), null);
|
|
assert.equal(parseISOLocal("02/20/2026"), null);
|
|
assert.equal(parseISOLocal(""), null);
|
|
assert.equal(parseISOLocal(undefined), null);
|
|
});
|