payments-dashboard/src/fetchBoaTransactions.js
Adam Moussa 44a6cd1b63 Add BoA CashPro integration and payment status tracking
- New fetchBoaTransactions Lambda: daily 9am ET schedule (disabled until API key set)
  Calls CashPro Previous Day Transaction Inquiry, filters for codes 255/475,
  matches bankReference to check_number, updates clear_status in DynamoDB
- CSV processor switched to UpdateCommand to preserve clearing data on re-upload
- Slack dashboard now shows Scheduled, Outstanding, Cleared, and Returned sections
- ACH payments auto-assumed cleared once past due date
2026-04-09 14:59:39 -04:00

123 lines
3.8 KiB
JavaScript

import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, ScanCommand, UpdateCommand } from "@aws-sdk/lib-dynamodb";
import { SSMClient, GetParameterCommand } from "@aws-sdk/client-ssm";
const ddb = DynamoDBDocumentClient.from(new DynamoDBClient());
const ssm = new SSMClient();
const TABLE_NAME = process.env.TABLE_NAME;
const BOA_BASE_URL = process.env.BOA_BASE_URL;
async function getSSMParam(name) {
const { Parameter } = await ssm.send(
new GetParameterCommand({ Name: name, WithDecryption: true })
);
return Parameter.Value;
}
export const handler = async () => {
const [apiToken, accountNumber] = await Promise.all([
getSSMParam(process.env.BOA_API_TOKEN_PARAM),
getSSMParam(process.env.BOA_ACCOUNT_NUMBER_PARAM),
]);
// Get yesterday's date in YYYY-MM-DD
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const dateStr = yesterday.toISOString().split("T")[0];
// Call CashPro Previous Day Transaction Inquiry
const res = await fetch(`${BOA_BASE_URL}/cashpro/reporting/v1/transaction-inquiries/previous-day`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiToken}`,
},
body: JSON.stringify({
accounts: [{ accountNumber, bankId: "BOFAFRPP" }],
fromDate: dateStr,
toDate: dateStr,
}),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`BoA API error ${res.status}: ${text}`);
}
const data = await res.json();
const transactions = data.accountTransactions?.transactions || [];
// Filter for cleared checks (255) and returned checks (475)
const relevant = transactions.filter(
(t) => t.transactionCode === "255" || t.transactionCode === "475"
);
if (!relevant.length) {
console.log(`No check transactions (255/475) found for ${dateStr}`);
return { statusCode: 200, body: `No relevant transactions for ${dateStr}` };
}
// Load all check payments from DynamoDB to match against
const payments = [];
let lastKey;
do {
const result = await ddb.send(
new ScanCommand({
TableName: TABLE_NAME,
FilterExpression: "begins_with(pk, :prefix) AND #m = :method",
ExpressionAttributeNames: { "#m": "method" },
ExpressionAttributeValues: { ":prefix": "payment#", ":method": "Check" },
ExclusiveStartKey: lastKey,
})
);
payments.push(...result.Items);
lastKey = result.LastEvaluatedKey;
} while (lastKey);
// Build a map of check_number -> payment for matching
const checkMap = new Map();
for (const p of payments) {
checkMap.set(p.check_number, p);
}
let matched = 0;
for (const txn of relevant) {
const bankRef = txn.bankReference || "";
// BoA may append extra digits to the reference number
// Try to find a check_number that the bankReference starts with
const matchedPayment = payments.find((p) =>
p.check_number && bankRef.startsWith(p.check_number)
);
if (!matchedPayment) {
console.log(`No match for bankReference: ${bankRef}`);
continue;
}
const clearStatus = txn.transactionCode === "255" ? "Cleared" : "Returned";
await ddb.send(
new UpdateCommand({
TableName: TABLE_NAME,
Key: { pk: matchedPayment.pk },
UpdateExpression: "SET clear_status = :status, bank_reference = :ref, cleared_date = :date",
ExpressionAttributeValues: {
":status": clearStatus,
":ref": bankRef,
":date": txn.valueDate || dateStr,
},
})
);
matched++;
console.log(`${clearStatus}: check ${matchedPayment.check_number} (bankRef: ${bankRef})`);
}
console.log(`Processed ${relevant.length} transactions, matched ${matched} payments`);
return {
statusCode: 200,
body: `${matched} of ${relevant.length} transactions matched to payments`,
};
};