const { DynamoDBClient } = require("@aws-sdk/client-dynamodb"); const { DynamoDBDocumentClient, ScanCommand } = require("@aws-sdk/lib-dynamodb"); const { parse } = require("csv-parse/sync"); const fs = require("fs"); const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({ region: "us-east-1" })); function toISODate(mdyDate) { const parts = String(mdyDate).split("/"); if (parts.length !== 3) return null; const [mm, dd, yyyy] = parts; return `${yyyy}-${mm.padStart(2, "0")}-${dd.padStart(2, "0")}`; } const cancelStatuses = ["voided", "cancelled", "canceled", "marked as void"]; (async () => { // Load all existing payments from DB const dbPayments = {}; let lastKey; do { const result = await ddb.send( new ScanCommand({ TableName: "PaymentsDashboard", FilterExpression: "begins_with(pk, :prefix)", ExpressionAttributeValues: { ":prefix": "payment#" }, ExclusiveStartKey: lastKey, }) ); for (const item of result.Items) { dbPayments[item.pk] = item; } lastKey = result.LastEvaluatedKey; } while (lastKey); // Read CSV from stdin const csvText = fs.readFileSync(0, "utf-8"); const rows = parse(csvText, { columns: true, skip_empty_lines: true, trim: true }); const normalizedRows = rows.map((row) => { const clean = {}; for (const [k, v] of Object.entries(row)) { clean[String(k).trim()] = typeof v === "string" ? v.trim() : v; } return clean; }); const parseAmount = (value) => { const num = parseFloat(String(value || "0").replace(/,/g, "").trim()); return isNaN(num) ? 0 : num; }; const statusRank = { "scheduled": 1, "payment submitted": 2, "issued": 3, "outstanding": 4, "cleared": 5, }; const newRecords = []; const statusChanges = []; const bankProtected = []; const statusProtected = []; const newCheckIssues = []; const cancelCheckIssues = []; let noChangeCount = 0; const today = new Date().toISOString().slice(0, 10); for (const row of normalizedRows) { const checkNumber = (row["Check Number"] || "").trim(); if (!checkNumber) continue; const method = (row["Method"] || "").trim(); let status = (row["Status"] || "").trim(); const sendOn = (row["Send Payment On"] || "").trim(); const payee = (row["Payee"] || "").trim(); const amount = parseAmount(row["Amount in USD"]); // ACH auto-clear logic if (method === "ACH" && !cancelStatuses.includes(status.toLowerCase())) { const sendDate = toISODate(sendOn); if (sendDate && sendDate <= today) { status = "Cleared"; } } const pk = "payment#" + checkNumber; const existing = dbPayments[pk]; // Bank-confirmed protection const bankConfirmed = existing?.clear_status === "Cleared"; let originalCsvStatus = status; if (bankConfirmed) { status = "Cleared"; } // Status progression protection if (existing) { const oldRank = statusRank[(existing.status || "").toLowerCase()] || 0; const newRank = statusRank[status.toLowerCase()] || 0; if (oldRank === 5) { // Cleared is permanent — cannot be voided, cancelled, or anything else if (status !== existing.status) { statusProtected.push({ checkNumber, payee, csvStatus: originalCsvStatus, dbStatus: existing.status, reason: "Cleared is permanent" }); } status = existing.status; } else if (newRank < oldRank && !cancelStatuses.includes(status.toLowerCase())) { // Non-cancel status regression — keep the existing (higher) status statusProtected.push({ checkNumber, payee, csvStatus: originalCsvStatus, dbStatus: existing.status, reason: "would regress status" }); status = existing.status; } } if (!existing) { newRecords.push({ checkNumber, payee, method, status, amount, sendOn }); if (method === "Check") { newCheckIssues.push({ checkNumber, payee, amount: amount.toFixed(2), issueDate: toISODate(sendOn) }); } } else { const oldStatus = existing.status || ""; if (bankConfirmed && originalCsvStatus !== "Cleared") { bankProtected.push({ checkNumber, payee, csvStatus: originalCsvStatus, dbStatus: oldStatus }); } if (oldStatus !== status) { statusChanges.push({ checkNumber, payee, method, oldStatus, newStatus: status, amount }); } else { noChangeCount++; } // Check for new cancel (only if not bank-confirmed and not cleared) if (method === "Check" && !bankConfirmed && (statusRank[(oldStatus).toLowerCase()] || 0) < 5 && cancelStatuses.includes(status.toLowerCase()) && !cancelStatuses.includes(oldStatus.toLowerCase())) { cancelCheckIssues.push({ checkNumber, payee, amount: amount.toFixed(2), issueDate: toISODate(sendOn) }); } } } console.log("=== DRY RUN SUMMARY ==="); console.log("CSV rows: " + normalizedRows.length); console.log("Existing DB records: " + Object.keys(dbPayments).length); console.log(""); if (newRecords.length) { console.log("--- NEW RECORDS (" + newRecords.length + ") ---"); for (const r of newRecords) { console.log(" + " + r.checkNumber + " | " + r.payee + " | " + r.method + " | " + r.status + " | $" + r.amount); } console.log(""); } if (statusChanges.length) { console.log("--- STATUS CHANGES (" + statusChanges.length + ") ---"); for (const r of statusChanges) { console.log(" ~ " + r.checkNumber + " | " + r.payee + " | " + r.method + " | \"" + r.oldStatus + "\" -> \"" + r.newStatus + "\" | $" + r.amount); } console.log(""); } if (statusProtected.length) { console.log("--- STATUS PROGRESSION PROTECTED (" + statusProtected.length + ") ---"); console.log(" (CSV tried to regress status — blocked by progression guard)"); for (const r of statusProtected) { console.log(" # " + r.checkNumber + " | " + r.payee + " | CSV: \"" + r.csvStatus + "\" | Kept: \"" + r.dbStatus + "\" | " + r.reason); } console.log(""); } if (bankProtected.length) { console.log("--- BANK-CONFIRMED PROTECTED (" + bankProtected.length + ") ---"); console.log(" (CSV tried to change status but bank already confirmed Cleared)"); for (const r of bankProtected) { console.log(" ! " + r.checkNumber + " | " + r.payee + " | CSV: \"" + r.csvStatus + "\" | Kept: Cleared"); } console.log(""); } if (newCheckIssues.length) { console.log("--- BOA: CHECK ISSUES / add_Issue (" + newCheckIssues.length + ") ---"); for (const r of newCheckIssues) { console.log(" >> " + r.checkNumber + " | " + r.payee + " | $" + r.amount + " | " + r.issueDate); } console.log(""); } if (cancelCheckIssues.length) { console.log("--- BOA: CHECK CANCELS / cancel_Issue (" + cancelCheckIssues.length + ") ---"); for (const r of cancelCheckIssues) { console.log(" XX " + r.checkNumber + " | " + r.payee + " | $" + r.amount + " | " + r.issueDate); } console.log(""); } console.log("=== TOTALS ==="); console.log("New DB records: " + newRecords.length); console.log("Status updates: " + statusChanges.length); console.log("Status-protected: " + statusProtected.length); console.log("Bank-protected: " + bankProtected.length); console.log("BoA add_Issue: " + newCheckIssues.length); console.log("BoA cancel_Issue: " + cancelCheckIssues.length); console.log("Unchanged: " + noChangeCount); })();