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`, }; };