payments-dashboard/scripts/seed-bank-status.js
Adam Moussa c43228831d Add unified payment status, ACH auto-clear, and categorized dashboard
- processPaymentCsv: switch from BatchWrite to upsert, auto-mark ACH
  payments as Cleared when send date has passed
- slackAppHome: categorize payments into Scheduled/Outstanding/Cleared
  sections using unified status field
- Add seed scripts for bulk-loading Stampli and bank CSV exports
2026-04-10 17:23:54 -04:00

141 lines
4.4 KiB
JavaScript

/**
* Seed bank status data from BoA exports into DynamoDB.
*
* Usage:
* node scripts/seed-bank-status.js \
* --cleared "Bank Export.csv" \
* --outstanding "PrintIssuesForInquiry.csv" \
* --issued "Positive Pay Import original.csv"
*
* All flags are optional — supply whichever files you have.
*
* Updates existing payment records:
* - status: overwritten to "Cleared" | "Outstanding" | "Issued"
* - paid_date: date the check cleared (from Bank Export)
*/
import { readFileSync } from "fs";
import { parse } from "csv-parse/sync";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, UpdateCommand } from "@aws-sdk/lib-dynamodb";
const TABLE_NAME = "PaymentsDashboard";
const ddb = DynamoDBDocumentClient.from(new DynamoDBClient());
function parseArgs() {
const args = process.argv.slice(2);
const result = {};
for (let i = 0; i < args.length; i += 2) {
const flag = args[i].replace(/^--/, "");
result[flag] = args[i + 1];
}
return result;
}
function readCSV(filePath) {
const text = readFileSync(filePath, "utf-8");
return parse(text, { columns: true, skip_empty_lines: true, trim: true });
}
// Normalize short dates like "1/27/26" → "2026-01-27"
function toISODate(dateStr) {
if (!dateStr) return null;
dateStr = dateStr.trim();
// Already ISO-ish: 04/13/2026
let parts = dateStr.split("/");
if (parts.length === 3) {
let [mm, dd, yyyy] = parts;
if (yyyy.length === 2) yyyy = `20${yyyy}`;
return `${yyyy}-${mm.padStart(2, "0")}-${dd.padStart(2, "0")}`;
}
return null;
}
async function updateStatus(checkNumber, status, paidDate) {
const pk = `payment#${checkNumber}`;
const expr = ["#status = :s"];
const names = { "#status": "status" };
const values = { ":s": status };
if (paidDate) {
expr.push("paid_date = :pd");
values[":pd"] = paidDate;
}
await ddb.send(
new UpdateCommand({
TableName: TABLE_NAME,
Key: { pk },
UpdateExpression: `SET ${expr.join(", ")}`,
ExpressionAttributeNames: names,
ExpressionAttributeValues: values,
})
);
}
async function main() {
const flags = parseArgs();
const statusMap = new Map(); // checkNumber → { status, paidDate }
// 1. Positive Pay Import (lowest priority) — "Issued"
// Format: check_number, date, ?, ?, amount (no headers)
if (flags.issued) {
console.log(`Reading positive pay import: ${flags.issued}`);
const text = readFileSync(flags.issued, "utf-8");
const rows = parse(text, { skip_empty_lines: true, trim: true });
for (const row of rows) {
const checkNumber = row[0]?.trim();
if (!checkNumber) continue;
statusMap.set(checkNumber, { status: "Issued", paidDate: null });
}
console.log(` ${rows.length} positive pay entries`);
}
// 2. Outstanding Issues (medium priority) — "Outstanding"
if (flags.outstanding) {
console.log(`Reading outstanding issues: ${flags.outstanding}`);
const rows = readCSV(flags.outstanding);
for (const row of rows) {
const checkNumber = (row["Check Number"] || "").trim();
if (!checkNumber) continue;
statusMap.set(checkNumber, { status: "Outstanding", paidDate: null });
}
console.log(` ${rows.length} outstanding entries`);
}
// 3. Bank Export / Cleared (highest priority) — "Cleared"
// Duplicates exist (same check appears twice); just take the first paid date.
if (flags.cleared) {
console.log(`Reading bank export (cleared): ${flags.cleared}`);
const rows = readCSV(flags.cleared);
const seen = new Set();
let deduped = 0;
for (const row of rows) {
const checkNumber = (row["Check Number"] || "").trim();
if (!checkNumber) continue;
if (seen.has(checkNumber)) {
deduped++;
continue;
}
seen.add(checkNumber);
const paidDate = toISODate(row["Paid Date"] || row["CD Volume Number"] || "");
statusMap.set(checkNumber, { status: "Cleared", paidDate });
}
console.log(` ${seen.size} unique cleared checks (${deduped} duplicates skipped)`);
}
// Write to DynamoDB
let count = 0;
for (const [checkNumber, { status, paidDate }] of statusMap) {
await updateStatus(checkNumber, status, paidDate);
count++;
}
console.log(`\nDone — ${count} payments updated with bank status.`);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});