mirror of
https://github.com/Sea-Haven-Industries/payments-dashboard.git
synced 2026-09-30 03:03:13 +00:00
fix: null-guard cash-position tile, add error logging, drop UTC date skew and serial GetItems
- 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
This commit is contained in:
parent
c7de44cc64
commit
6af1a6ab90
2 changed files with 65 additions and 31 deletions
|
|
@ -3,7 +3,7 @@ import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|||
import { DynamoDBDocumentClient, GetCommand, ScanCommand } from "@aws-sdk/lib-dynamodb";
|
||||
import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";
|
||||
import { parseISOLocal, parseMDYLocal } from "./dates.js";
|
||||
import { isCancelStatus } from "./boaRecon.js";
|
||||
import { isCancelStatus, logSafe } from "./boaRecon.js";
|
||||
|
||||
const ddb = DynamoDBDocumentClient.from(new DynamoDBClient());
|
||||
const secrets = new SecretsManagerClient();
|
||||
|
|
@ -405,39 +405,62 @@ async function scanPayments() {
|
|||
// --- Read the latest previous-day balance snapshot ---
|
||||
|
||||
async function fetchCashPosition() {
|
||||
const today = new Date();
|
||||
const todayStr = today.toISOString().split("T")[0];
|
||||
const now = new Date();
|
||||
// Local-midnight today so date arithmetic stays in local time: toISOString()
|
||||
// gives UTC, which from 8pm ET is tomorrow's date (#80-review).
|
||||
const localToday = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
|
||||
// Walk backward from today looking for a previous-day balance snapshot.
|
||||
// Weekends and holidays may leave gaps; scan up to 14 days back.
|
||||
let previousDay = null;
|
||||
for (let i = 0; i < 14; i++) {
|
||||
const d = new Date(today);
|
||||
const toDateStr = (d) =>
|
||||
d.getFullYear() + "-" +
|
||||
String(d.getMonth() + 1).padStart(2, "0") + "-" +
|
||||
String(d.getDate()).padStart(2, "0");
|
||||
|
||||
const todayStr = toDateStr(localToday);
|
||||
|
||||
// Intraday (today, current-day) + up to 13 prior previous-day snapshots,
|
||||
// all fetched in parallel. A previous-day snapshot dated today cannot exist
|
||||
// until tomorrow, so the walk starts at i = 1 (#80-review).
|
||||
const keys = [{ key: `boa_balance#${todayStr}#current-day` }];
|
||||
for (let i = 1; i < 14; i++) {
|
||||
const d = new Date(localToday);
|
||||
d.setDate(d.getDate() - i);
|
||||
const dateStr = d.toISOString().split("T")[0];
|
||||
const key = `boa_balance#${dateStr}#previous-day`;
|
||||
try {
|
||||
const { Item } = await ddb.send(
|
||||
new GetCommand({ TableName: TABLE_NAME, Key: { pk: key } })
|
||||
);
|
||||
if (Item) {
|
||||
previousDay = { ...Item, _asOfDate: dateStr };
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// GetCommand failure is non-fatal; move to the prior day.
|
||||
}
|
||||
const ds = toDateStr(d);
|
||||
keys.push({ key: `boa_balance#${ds}#previous-day` });
|
||||
}
|
||||
|
||||
// Try today's intraday snapshot (current-day) for optional provisional display.
|
||||
const results = await Promise.allSettled(
|
||||
keys.map(({ key }) =>
|
||||
ddb.send(new GetCommand({ TableName: TABLE_NAME, Key: { pk: key } }))
|
||||
)
|
||||
);
|
||||
|
||||
let intraday = null;
|
||||
try {
|
||||
const { Item } = await ddb.send(
|
||||
new GetCommand({ TableName: TABLE_NAME, Key: { pk: `boa_balance#${todayStr}#current-day` } })
|
||||
if (results[0].status === "fulfilled" && results[0].value.Item) {
|
||||
intraday = results[0].value.Item;
|
||||
} else if (results[0].status === "rejected") {
|
||||
console.error(
|
||||
"Cash-position intraday GetItem failed:",
|
||||
logSafe(results[0].reason?.name),
|
||||
logSafe(results[0].reason?.message)
|
||||
);
|
||||
if (Item) intraday = { ...Item, _asOfDate: todayStr };
|
||||
} catch {
|
||||
// Non-fatal.
|
||||
}
|
||||
|
||||
// Walk previous-day results (indices 1..) newest-first; pick the first hit.
|
||||
let previousDay = null;
|
||||
for (let i = 1; i < results.length; i++) {
|
||||
const res = results[i];
|
||||
if (res.status === "rejected") {
|
||||
console.error(
|
||||
"Cash-position previous-day GetItem failed:",
|
||||
logSafe(res.reason?.name),
|
||||
logSafe(res.reason?.message)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (res.value.Item) {
|
||||
previousDay = res.value.Item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { previousDay, intraday };
|
||||
|
|
@ -728,8 +751,8 @@ export function buildHomeView(payments, metadata, boaTransactions = [], cashPosi
|
|||
fields: [
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: balance
|
||||
? `:moneybag: *Cash Position* — ${balance._asOfDate}\nLedger ${formatCurrency(balance.current_ledger)} · Available ${formatCurrency(balance.current_available)}`
|
||||
text: balance && balance.current_ledger != null
|
||||
? `:moneybag: *Cash Position* — ${balance.as_of_date}\nLedger ${formatCurrency(balance.current_ledger)} · Available ${formatCurrency(balance.current_available)}`
|
||||
: `:moneybag: *Cash Position*\nNot available`,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -144,7 +144,6 @@ const balanceItem = (overrides = {}) => ({
|
|||
total_debits_count: 5,
|
||||
total_ach_debits: 20650.65,
|
||||
other: {},
|
||||
_asOfDate: "2026-07-22",
|
||||
...overrides,
|
||||
});
|
||||
|
||||
|
|
@ -173,6 +172,18 @@ test("home view: cash position shows 'Not available' when balance is missing", (
|
|||
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(),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue