payments-dashboard/src/fetchBoaTransactions.js

146 lines
4.6 KiB
JavaScript
Raw Normal View History

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;
}
async function getAccessToken(applicationID, clientId, clientSecret) {
const res = await fetch(`${BOA_BASE_URL}/authn/v1/client-authentication`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
applicationID,
authn: { client_id: clientId, client_secret: clientSecret },
}),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`OAuth token exchange failed: ${res.status} - ${text}`);
}
const data = await res.json();
return data.access_token;
}
export const handler = async () => {
const [appId, clientId, clientSecret, accountNumber, bankId] = await Promise.all([
getSSMParam(process.env.BOA_REPORTING_APP_ID_PARAM),
getSSMParam(process.env.BOA_REPORTING_CLIENT_ID_PARAM),
getSSMParam(process.env.BOA_REPORTING_SECRET_PARAM),
getSSMParam(process.env.BOA_ACCOUNT_NUMBER_PARAM),
getSSMParam(process.env.BOA_BANK_ID_PARAM),
]);
const bearerToken = await getAccessToken(appId, clientId, clientSecret);
// 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 ${bearerToken}`,
},
body: JSON.stringify({
fromDate: dateStr,
toDate: dateStr,
accounts: [{ accountNumber, bankId }],
}),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`BoA API error ${res.status}: ${text}`);
}
const data = await res.json();
// Response shape: { accountTransactions: [{ accountNumber, bankId, currency, transactions: [...] }] }
const allTransactions = (data.accountTransactions || []).flatMap(
(acct) => acct.transactions || []
);
// Filter for cleared checks (255) and returned checks (475)
const relevant = allTransactions.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);
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`,
};
};