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 "); 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}`); })();