/** * Seed DynamoDB PaymentsDashboard table from Stampli CSV exports. * * Usage: * node scripts/seed-from-csv.js [csv-file2] ... * * Reads one or more Stampli CSV exports and upserts every row into the * PaymentsDashboard table. Skips CashPro API calls entirely — this is * purely for populating the DB so the Slack App Home has data to display. */ import { readFileSync } from "fs"; import { parse } from "csv-parse/sync"; import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { DynamoDBDocumentClient, UpdateCommand, PutCommand, } from "@aws-sdk/lib-dynamodb"; const TABLE_NAME = "PaymentsDashboard"; const ddb = DynamoDBDocumentClient.from(new DynamoDBClient()); 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 parseAmount = (value) => { const num = parseFloat(String(value || "0").replace(/,/g, "").trim()); return isNaN(num) ? 0 : num; }; async function seedFile(filePath) { const csvText = readFileSync(filePath, "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; }); let count = 0; let skipped = 0; for (const row of normalizedRows) { const checkNumber = (row["Check Number"] || "").trim(); if (!checkNumber) { skipped++; continue; } const method = (row["Method"] || "").trim(); let status = (row["Status"] || "").trim(); const sendOn = (row["Send Payment On"] || "").trim(); // ACH payments clear automatically on their send date — // mark as Cleared unless voided/canceled const cancelStatuses = ["canceled", "cancelled", "marked as void"]; if (method === "ACH" && !cancelStatuses.includes(status.toLowerCase())) { const sendDate = toISODate(sendOn); const today = new Date().toISOString().slice(0, 10); if (sendDate && sendDate <= today) { status = "Cleared"; } } // Amount in USD is blank for canceled payments — fall back to Amount const amount = parseAmount(row["Amount in USD"] || row["Amount"]); const pk = `payment#${checkNumber}`; await ddb.send( new UpdateCommand({ TableName: TABLE_NAME, Key: { pk }, UpdateExpression: ` SET #method = :method, payee = :payee, check_number = :check_number, invoice_numbers = :invoice_numbers, send_payment_on = :send_payment_on, amount_usd = :amount_usd, #status = :status, company_subsidiary = :company_subsidiary `, ExpressionAttributeNames: { "#method": "method", "#status": "status", }, ExpressionAttributeValues: { ":method": method, ":payee": (row["Payee"] || "").trim(), ":check_number": checkNumber, ":invoice_numbers": (row["Invoice Numbers"] || "").trim(), ":send_payment_on": (row["Send Payment On"] || "").trim(), ":amount_usd": amount, ":status": status, ":company_subsidiary": (row["Company/Subsidiary"] || "").trim(), }, }) ); count++; } console.log(` ${filePath}: ${count} upserted, ${skipped} skipped (no check number)`); return count; } async function main() { const files = process.argv.slice(2); if (!files.length) { console.error("Usage: node scripts/seed-from-csv.js [csv-file2] ..."); process.exit(1); } let total = 0; for (const f of files) { console.log(`Processing ${f}...`); total += await seedFile(f); } // Update metadata await ddb.send( new PutCommand({ TableName: TABLE_NAME, Item: { pk: "metadata", file_name: "seed-from-csv", last_updated: new Date().toISOString(), last_file_count: total, }, }) ); console.log(`\nDone — ${total} total payments seeded.`); } main().catch((err) => { console.error(err); process.exit(1); });