mirror of
https://github.com/Sea-Haven-Industries/payments-dashboard.git
synced 2026-09-30 22:53:11 +00:00
117 lines
5.4 KiB
JavaScript
117 lines
5.4 KiB
JavaScript
|
|
const fs = require("fs");
|
||
|
|
const { parse } = require("csv-parse/sync");
|
||
|
|
const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");
|
||
|
|
const { DynamoDBDocumentClient, BatchGetCommand } = require("@aws-sdk/lib-dynamodb");
|
||
|
|
|
||
|
|
const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({ region: "us-east-1" }));
|
||
|
|
const CSV_PATH = process.argv[2];
|
||
|
|
if (!CSV_PATH) { console.error("Usage: node simulate-csv.cjs <csv-path>"); process.exit(1); }
|
||
|
|
|
||
|
|
const cancelStatuses = ["voided", "cancelled", "canceled", "marked as void"];
|
||
|
|
const statusRank = { "scheduled": 1, "payment submitted": 2, "issued": 3, "outstanding": 4, "cleared": 5 };
|
||
|
|
|
||
|
|
function toISODate(mdy) {
|
||
|
|
const p = String(mdy).split("/");
|
||
|
|
if (p.length !== 3) return null;
|
||
|
|
const [mm, dd, yyyy] = p;
|
||
|
|
return `${yyyy}-${mm.padStart(2, "0")}-${dd.padStart(2, "0")}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
(async () => {
|
||
|
|
const text = fs.readFileSync(CSV_PATH, "utf-8");
|
||
|
|
const rows = parse(text, { columns: true, skip_empty_lines: true, trim: true })
|
||
|
|
.map((r) => Object.fromEntries(Object.entries(r).map(([k, v]) => [String(k).trim(), typeof v === "string" ? v.trim() : v])));
|
||
|
|
|
||
|
|
const validRows = rows.filter((r) => (r["Check Number"] || "").trim());
|
||
|
|
console.log(`CSV rows: ${rows.length} total, ${validRows.length} with check numbers`);
|
||
|
|
|
||
|
|
// Batch get existing records
|
||
|
|
const keys = validRows.map((r) => ({ pk: `payment#${r["Check Number"].trim()}` }));
|
||
|
|
const existing = {};
|
||
|
|
for (let i = 0; i < keys.length; i += 100) {
|
||
|
|
const batch = keys.slice(i, i + 100);
|
||
|
|
const res = await ddb.send(new BatchGetCommand({ RequestItems: { PaymentsDashboard: { Keys: batch } } }));
|
||
|
|
for (const item of res.Responses.PaymentsDashboard) existing[item.pk] = item;
|
||
|
|
}
|
||
|
|
console.log(`DDB records found for: ${Object.keys(existing).length} of ${validRows.length}\n`);
|
||
|
|
|
||
|
|
const today = new Date().toISOString().slice(0, 10);
|
||
|
|
const newChecks = [];
|
||
|
|
const cancelChecks = [];
|
||
|
|
const statusChanges = [];
|
||
|
|
const frozenByBank = [];
|
||
|
|
const frozenByRank = [];
|
||
|
|
const newNonChecks = [];
|
||
|
|
|
||
|
|
for (const row of validRows) {
|
||
|
|
const checkNumber = row["Check Number"].trim();
|
||
|
|
const method = (row["Method"] || "").trim();
|
||
|
|
let status = (row["Status"] || "").trim();
|
||
|
|
const sendOn = (row["Send Payment On"] || "").trim();
|
||
|
|
|
||
|
|
if (method === "ACH" && !cancelStatuses.includes(status.toLowerCase())) {
|
||
|
|
const sendDate = toISODate(sendOn);
|
||
|
|
if (sendDate && sendDate <= today) status = "Cleared";
|
||
|
|
}
|
||
|
|
|
||
|
|
const pk = `payment#${checkNumber}`;
|
||
|
|
const ex = existing[pk];
|
||
|
|
|
||
|
|
const bankConfirmed = ex?.clear_status === "Cleared";
|
||
|
|
if (bankConfirmed) status = "Cleared";
|
||
|
|
|
||
|
|
let frozenReason = null;
|
||
|
|
if (ex) {
|
||
|
|
const oldRank = statusRank[(ex.status || "").toLowerCase()] || 0;
|
||
|
|
const newRank = statusRank[status.toLowerCase()] || 0;
|
||
|
|
if (oldRank === 5) {
|
||
|
|
if (status !== ex.status) frozenReason = `cleared→${status} blocked`;
|
||
|
|
status = ex.status;
|
||
|
|
} else if (newRank < oldRank && !cancelStatuses.includes(status.toLowerCase())) {
|
||
|
|
if (status !== ex.status) frozenReason = `${ex.status}→${status} regressed`;
|
||
|
|
status = ex.status;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (frozenReason) {
|
||
|
|
if (bankConfirmed) frozenByBank.push({ checkNumber, payee: row["Payee"], reason: frozenReason });
|
||
|
|
else frozenByRank.push({ checkNumber, payee: row["Payee"], reason: frozenReason });
|
||
|
|
} else if (ex && ex.status !== status) {
|
||
|
|
statusChanges.push({ checkNumber, payee: row["Payee"], from: ex.status, to: status });
|
||
|
|
}
|
||
|
|
|
||
|
|
if (method !== "Check") {
|
||
|
|
if (!ex) newNonChecks.push({ checkNumber, payee: row["Payee"], method, status, amount: row["Amount in USD"] });
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!ex) {
|
||
|
|
newChecks.push({ checkNumber, payee: row["Payee"], amount: row["Amount in USD"], status, sendOn });
|
||
|
|
} else if (cancelStatuses.includes(status.toLowerCase()) && !cancelStatuses.includes((ex.status || "").toLowerCase())) {
|
||
|
|
cancelChecks.push({ checkNumber, payee: row["Payee"], from: ex.status, amount: row["Amount in USD"] });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log(`========= WOULD HIT BoA =========`);
|
||
|
|
console.log(`NEW checks → add_Issue: ${newChecks.length}`);
|
||
|
|
for (const c of newChecks) console.log(` ${c.checkNumber} | ${c.payee} | $${c.amount} | ${c.status} | ${c.sendOn}`);
|
||
|
|
|
||
|
|
console.log(`\nCancellations → cancel_Issue: ${cancelChecks.length}`);
|
||
|
|
for (const c of cancelChecks) console.log(` ${c.checkNumber} | ${c.payee} | ${c.from}→cancel | $${c.amount}`);
|
||
|
|
|
||
|
|
console.log(`\n========= DDB-ONLY CHANGES =========`);
|
||
|
|
console.log(`Status changes (existing records): ${statusChanges.length}`);
|
||
|
|
for (const c of statusChanges.slice(0, 30)) console.log(` ${c.checkNumber} | ${c.payee} | ${c.from} → ${c.to}`);
|
||
|
|
if (statusChanges.length > 30) console.log(` ... +${statusChanges.length - 30} more`);
|
||
|
|
|
||
|
|
console.log(`\nNew non-check rows (ACH/etc., DDB only): ${newNonChecks.length}`);
|
||
|
|
for (const c of newNonChecks.slice(0, 10)) console.log(` ${c.checkNumber} | ${c.payee} | ${c.method} | ${c.status} | $${c.amount}`);
|
||
|
|
if (newNonChecks.length > 10) console.log(` ... +${newNonChecks.length - 10} more`);
|
||
|
|
|
||
|
|
console.log(`\n========= BLOCKED / FROZEN =========`);
|
||
|
|
console.log(`Frozen by bank-confirmed Cleared: ${frozenByBank.length}`);
|
||
|
|
for (const c of frozenByBank.slice(0, 10)) console.log(` ${c.checkNumber} | ${c.payee} | ${c.reason}`);
|
||
|
|
console.log(`\nFrozen by status-rank protection: ${frozenByRank.length}`);
|
||
|
|
for (const c of frozenByRank.slice(0, 10)) console.log(` ${c.checkNumber} | ${c.payee} | ${c.reason}`);
|
||
|
|
})();
|