/** * 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, amount, issueDate, method } = {}) { 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; } if (amount != null) { expr.push("amount_usd = :amt"); values[":amt"] = amount; } if (issueDate) { expr.push("send_payment_on = :sd"); values[":sd"] = issueDate; } if (checkNumber) { expr.push("check_number = :cn"); values[":cn"] = checkNumber; } if (method) { expr.push("#method = :m"); names["#method"] = "method"; values[":m"] = method; } 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; const amount = parseFloat(String(row[4] || "0").replace(/,/g, "")); const issueDate = row[1]?.trim() || null; statusMap.set(checkNumber, { status: "Issued", paidDate: null, amount: isNaN(amount) ? 0 : amount, issueDate, method: "Check" }); } 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; const amount = parseFloat(String(row["Amount"] || "0").replace(/,/g, "")); const rawDate = (row["Issue Date"] || "").trim(); // Normalize 2-digit year to 4-digit: "12/10/24" → "12/10/2024" let issueDate = rawDate || null; if (rawDate) { const parts = rawDate.split("/"); if (parts.length === 3 && parts[2].length === 2) { parts[2] = `20${parts[2]}`; issueDate = parts.join("/"); } } statusMap.set(checkNumber, { status: "Outstanding", paidDate: null, amount: isNaN(amount) ? 0 : amount, issueDate, method: "Check" }); } 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"] || ""); const amount = parseFloat(String(row["Amount"] || "0").replace(/,/g, "")); statusMap.set(checkNumber, { status: "Cleared", paidDate, amount: isNaN(amount) ? 0 : amount }); } console.log(` ${seen.size} unique cleared checks (${deduped} duplicates skipped)`); } // Write to DynamoDB let count = 0; for (const [checkNumber, { status, paidDate, amount, issueDate, method }] of statusMap) { await updateStatus(checkNumber, status, { paidDate, amount, issueDate, method }); count++; } console.log(`\nDone — ${count} payments updated with bank status.`); } main().catch((err) => { console.error(err); process.exit(1); });