mirror of
https://github.com/Sea-Haven-Industries/payments-dashboard.git
synced 2026-09-30 05:23:12 +00:00
Add unified payment status, ACH auto-clear, and categorized dashboard
- 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
This commit is contained in:
parent
7150028bd3
commit
c43228831d
5 changed files with 414 additions and 78 deletions
141
scripts/seed-bank-status.js
Normal file
141
scripts/seed-bank-status.js
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
/**
|
||||
* 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) {
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
statusMap.set(checkNumber, { status: "Issued", paidDate: null });
|
||||
}
|
||||
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;
|
||||
statusMap.set(checkNumber, { status: "Outstanding", paidDate: null });
|
||||
}
|
||||
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"] || "");
|
||||
statusMap.set(checkNumber, { status: "Cleared", paidDate });
|
||||
}
|
||||
console.log(` ${seen.size} unique cleared checks (${deduped} duplicates skipped)`);
|
||||
}
|
||||
|
||||
// Write to DynamoDB
|
||||
let count = 0;
|
||||
for (const [checkNumber, { status, paidDate }] of statusMap) {
|
||||
await updateStatus(checkNumber, status, paidDate);
|
||||
count++;
|
||||
}
|
||||
|
||||
console.log(`\nDone — ${count} payments updated with bank status.`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
150
scripts/seed-from-csv.js
Normal file
150
scripts/seed-from-csv.js
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
/**
|
||||
* 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);
|
||||
});
|
||||
|
|
@ -1,12 +1,20 @@
|
|||
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
|
||||
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
||||
import { DynamoDBDocumentClient, BatchWriteCommand, PutCommand } from "@aws-sdk/lib-dynamodb";
|
||||
import { DynamoDBDocumentClient, UpdateCommand, PutCommand } from "@aws-sdk/lib-dynamodb";
|
||||
import { parse } from "csv-parse/sync";
|
||||
|
||||
const s3 = new S3Client();
|
||||
const ddb = DynamoDBDocumentClient.from(new DynamoDBClient());
|
||||
const TABLE_NAME = process.env.TABLE_NAME;
|
||||
|
||||
// Convert MM/DD/YYYY to YYYY-MM-DD
|
||||
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")}`;
|
||||
}
|
||||
|
||||
export const handler = async (event) => {
|
||||
const record = event.Records[0];
|
||||
const bucket = record.s3.bucket.name;
|
||||
|
|
@ -36,33 +44,60 @@ export const handler = async (event) => {
|
|||
return isNaN(num) ? 0 : num;
|
||||
};
|
||||
|
||||
const payments = normalizedRows.map((row) => ({
|
||||
pk: `payment#${(row["Check Number"] || "").trim()}`,
|
||||
method: (row["Method"] || "").trim(),
|
||||
payee: (row["Payee"] || "").trim(),
|
||||
check_number: (row["Check Number"] || "").trim(),
|
||||
invoice_numbers: (row["Invoice Numbers"] || "").trim(),
|
||||
send_payment_on: (row["Send Payment On"] || "").trim(),
|
||||
amount_usd: parseAmount(row["Amount in USD"]),
|
||||
status: (row["Status"] || "").trim(),
|
||||
company_subsidiary: (row["Company/Subsidiary"] || "").trim(),
|
||||
}));
|
||||
const cancelStatuses = ["voided", "cancelled", "canceled", "marked as void"];
|
||||
|
||||
// Write payments in batches of 25 (DynamoDB BatchWrite limit)
|
||||
const batches = [];
|
||||
for (let i = 0; i < payments.length; i += 25) {
|
||||
const batch = payments.slice(i, i + 25).map((item) => ({
|
||||
PutRequest: { Item: item },
|
||||
}));
|
||||
batches.push(batch);
|
||||
}
|
||||
// Upsert each payment
|
||||
let count = 0;
|
||||
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();
|
||||
|
||||
// ACH payments clear automatically on their send date
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
const pk = `payment#${checkNumber}`;
|
||||
|
||||
for (const batch of batches) {
|
||||
await ddb.send(
|
||||
new BatchWriteCommand({
|
||||
RequestItems: { [TABLE_NAME]: batch },
|
||||
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": sendOn,
|
||||
":amount_usd": parseAmount(row["Amount in USD"]),
|
||||
":status": status,
|
||||
":company_subsidiary": (row["Company/Subsidiary"] || "").trim(),
|
||||
},
|
||||
})
|
||||
);
|
||||
count++;
|
||||
}
|
||||
|
||||
// Update metadata record
|
||||
|
|
@ -73,11 +108,11 @@ export const handler = async (event) => {
|
|||
pk: "metadata",
|
||||
file_name: key.split("/").pop(),
|
||||
last_updated: new Date().toISOString(),
|
||||
last_file_count: payments.length,
|
||||
last_file_count: count,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
console.log(`Upserted ${payments.length} payments from ${key}`);
|
||||
return { statusCode: 200, body: `Upserted ${payments.length} payments` };
|
||||
console.log(`Upserted ${count} payments from ${key}`);
|
||||
return { statusCode: 200, body: `Upserted ${count} payments` };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -83,7 +83,6 @@ export const handler = async (event) => {
|
|||
};
|
||||
|
||||
function buildHomeView(payments, metadata) {
|
||||
|
||||
const formatCurrency = (value) =>
|
||||
new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(
|
||||
Number(value || 0)
|
||||
|
|
@ -98,88 +97,97 @@ function buildHomeView(payments, metadata) {
|
|||
return new Date(yyyy, mm - 1, dd);
|
||||
};
|
||||
|
||||
const formatDateKey = (date) => {
|
||||
const y = date.getFullYear();
|
||||
const m = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const d = String(date.getDate()).padStart(2, "0");
|
||||
return `${y}-${m}-${d}`;
|
||||
};
|
||||
|
||||
const formatDisplayDate = (dateKey) => {
|
||||
const [y, m, d] = dateKey.split("-").map(Number);
|
||||
const dt = new Date(y, m - 1, d);
|
||||
return dt.toLocaleDateString("en-US", {
|
||||
weekday: "short",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
};
|
||||
const formatDisplayDate = (date) =>
|
||||
date.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" });
|
||||
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const futurePayments = payments.filter((p) => {
|
||||
// Categorize payments by status
|
||||
const scheduled = [];
|
||||
const outstanding = [];
|
||||
const cleared = [];
|
||||
|
||||
const skipStatuses = ["voided", "cancelled", "canceled", "marked as void"];
|
||||
|
||||
for (const p of payments) {
|
||||
if (p.method !== "ACH" && p.method !== "Check") continue;
|
||||
|
||||
const dt = parseMDYLocal(p.send_payment_on);
|
||||
return dt && dt >= today && (p.method === "ACH" || p.method === "Check");
|
||||
});
|
||||
if (!dt) continue;
|
||||
|
||||
const groupByMethodAndDay = (methodName) => {
|
||||
const grouped = {};
|
||||
const status = (p.status || "").toLowerCase();
|
||||
|
||||
for (const payment of futurePayments) {
|
||||
if (payment.method !== methodName) continue;
|
||||
const dt = parseMDYLocal(payment.send_payment_on);
|
||||
if (!dt) continue;
|
||||
const key = formatDateKey(dt);
|
||||
// Skip voided/cancelled payments entirely
|
||||
if (skipStatuses.includes(status)) continue;
|
||||
|
||||
if (!grouped[key]) grouped[key] = { count: 0, total: 0 };
|
||||
grouped[key].count += 1;
|
||||
grouped[key].total += payment.amount_usd;
|
||||
if (status === "cleared") {
|
||||
cleared.push(p);
|
||||
} else if (status === "outstanding") {
|
||||
outstanding.push(p);
|
||||
} else if (dt < today) {
|
||||
// Past due date but not yet confirmed cleared — outstanding
|
||||
outstanding.push(p);
|
||||
} else {
|
||||
scheduled.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(grouped)
|
||||
.sort()
|
||||
.map((dateKey) => ({
|
||||
date_key: dateKey,
|
||||
display_date: formatDisplayDate(dateKey),
|
||||
count: grouped[dateKey].count,
|
||||
total: Number(grouped[dateKey].total.toFixed(2)),
|
||||
}));
|
||||
// Sort by date
|
||||
const byDate = (a, b) => {
|
||||
const da = parseMDYLocal(a.send_payment_on);
|
||||
const db = parseMDYLocal(b.send_payment_on);
|
||||
return (da || 0) - (db || 0);
|
||||
};
|
||||
scheduled.sort(byDate);
|
||||
outstanding.sort(byDate);
|
||||
cleared.sort(byDate);
|
||||
|
||||
const achDays = groupByMethodAndDay("ACH");
|
||||
const checkDays = groupByMethodAndDay("Check");
|
||||
|
||||
const buildDayBlocks = (title, days) => {
|
||||
const total = days.reduce((sum, d) => sum + d.total, 0);
|
||||
const count = days.reduce((sum, d) => sum + d.count, 0);
|
||||
const buildSectionBlocks = (title, emoji, items) => {
|
||||
const total = items.reduce((sum, p) => sum + p.amount_usd, 0);
|
||||
|
||||
const blocks = [
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `*${title}*\n${count} payments • ${formatCurrency(total)}`,
|
||||
text: `*${emoji} ${title}*\n${items.length} payments • ${formatCurrency(total)}`,
|
||||
},
|
||||
},
|
||||
{ type: "divider" },
|
||||
];
|
||||
|
||||
if (!days.length) {
|
||||
if (!items.length) {
|
||||
blocks.push({
|
||||
type: "section",
|
||||
text: { type: "mrkdwn", text: "_No upcoming payments found._" },
|
||||
text: { type: "mrkdwn", text: "_None_" },
|
||||
});
|
||||
blocks.push({ type: "divider" });
|
||||
return blocks;
|
||||
}
|
||||
|
||||
for (const day of days) {
|
||||
// Group by date
|
||||
const grouped = {};
|
||||
for (const p of items) {
|
||||
const dt = parseMDYLocal(p.send_payment_on);
|
||||
const key = dt ? dt.toISOString().split("T")[0] : "unknown";
|
||||
if (!grouped[key]) grouped[key] = { date: dt, payments: [] };
|
||||
grouped[key].payments.push(p);
|
||||
}
|
||||
|
||||
for (const key of Object.keys(grouped).sort()) {
|
||||
const group = grouped[key];
|
||||
const dayTotal = group.payments.reduce((sum, p) => sum + p.amount_usd, 0);
|
||||
const dateLabel = group.date ? formatDisplayDate(group.date) : "Unknown";
|
||||
|
||||
blocks.push({
|
||||
type: "section",
|
||||
fields: [
|
||||
{ type: "mrkdwn", text: `*${day.display_date}*` },
|
||||
{ type: "mrkdwn", text: `${day.count} payments • ${formatCurrency(day.total)}` },
|
||||
{ type: "mrkdwn", text: `*${dateLabel}*` },
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: `${group.payments.length} payments • ${formatCurrency(dayTotal)}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
|
@ -203,8 +211,9 @@ function buildHomeView(payments, metadata) {
|
|||
],
|
||||
},
|
||||
{ type: "divider" },
|
||||
...buildDayBlocks("ACH", achDays),
|
||||
...buildDayBlocks("Check", checkDays),
|
||||
...buildSectionBlocks("Scheduled", ":calendar:", scheduled),
|
||||
...buildSectionBlocks("Outstanding", ":warning:", outstanding),
|
||||
...buildSectionBlocks("Cleared", ":white_check_mark:", cleared),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ Resources:
|
|||
Properties:
|
||||
FunctionName: payments-processPaymentCsv
|
||||
Handler: src/processPaymentCsv.handler
|
||||
Timeout: 120
|
||||
VpcConfig:
|
||||
SubnetIds:
|
||||
- !Ref PrivateSubnet
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue