mirror of
https://github.com/Sea-Haven-Industries/payments-dashboard.git
synced 2026-09-30 19:23:12 +00:00
- 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
150 lines
4.2 KiB
JavaScript
150 lines
4.2 KiB
JavaScript
/**
|
|
* Seed DynamoDB PaymentsDashboard table from Stampli CSV exports.
|
|
*
|
|
* Usage:
|
|
* node scripts/seed-from-csv.js <csv-file> [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-file> [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);
|
|
});
|