From c445fa947a2799d47d3340b8dd5703556c0daaa5 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Mon, 13 Apr 2026 16:24:20 -0400 Subject: [PATCH] Update integration code with correct BoA CashPro API specs and add README - Add OAuth client-credentials token exchange to both Lambda handlers - Fix sandbox/prod base URL (api-sb.bofa.com / api.bofa.com) - Fix issueAction casing to add_Issue / cancel_Issue per API docs - Fix transaction inquiry response parsing (accountTransactions array) - Move all BoA config (app IDs, bank ID) from env vars to SSM params - Update template.yaml with correct SSM param names and policies - Add project README with architecture, API details, and SSM param reference --- README.md | 63 +++++++++++++++++++++++++++++++++++++ src/fetchBoaTransactions.js | 46 ++++++++++++++++++++------- src/processPaymentCsv.js | 35 +++++++++++++++++---- template.yaml | 28 ++++++++++++----- 4 files changed, 146 insertions(+), 26 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..99dccd6 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# Payments Dashboard + +AWS SAM application that ingests payment CSVs, syncs check data with Bank of America CashPro APIs, and surfaces an outstanding-payments dashboard in Slack. + +## Architecture + +- **ProcessPaymentCsv** - Lambda triggered by S3 CSV upload. Parses payments, upserts to DynamoDB, and submits new/cancelled checks to the CashPro Check Management API. +- **FetchBoaTransactions** - Scheduled Lambda (weekdays 9am ET). Calls the CashPro Previous Day Transaction Inquiry API and matches cleared/returned checks back to DynamoDB records. +- **SlackAppHome** - Lambda behind API Gateway. Renders the payments dashboard on the Slack App Home tab with outstanding aging buckets and drill-down modals. + +All three Lambdas run inside a VPC with a NAT Gateway for a static outbound IP (required by BoA IP whitelisting). + +## BoA CashPro API Integration + +Two separate CashPro APIs are used, each with its own OAuth credentials: + +| API | Purpose | Endpoint | +|-----|---------|----------| +| Check Management | Issue and cancel checks | `/cashpro/checkmanagement/v1/check-issues` | +| Reporting (Transaction Inquiry) | Fetch previous-day transactions | `/cashpro/reporting/v1/transaction-inquiries/previous-day` | + +**Authentication flow:** +1. POST to `/authn/v1/client-authentication` with `applicationID`, `client_id`, and `client_secret` +2. Receive a Bearer `access_token` (valid 1 hour) +3. Pass the token in the `Authorization` header for subsequent API calls + +**Base URLs:** +- Production: `https://api.bofa.com` +- Sandbox: `https://api-sb.bofa.com` + +## SSM Parameters + +All BoA credentials and config are stored in AWS SSM Parameter Store (SecureString): + +| Parameter | Description | +|-----------|-------------| +| `/payments-dashboard/boa-check-mgmt-app-id` | Check Management application ID | +| `/payments-dashboard/boa-check-mgmt-client-id` | Check Management client ID | +| `/payments-dashboard/boa-check-mgmt-token` | Check Management client secret | +| `/payments-dashboard/boa-reporting-app-id` | Reporting application ID | +| `/payments-dashboard/boa-account-info-client-id` | Reporting client ID | +| `/payments-dashboard/boa-account-info-token` | Reporting client secret | +| `/payments-dashboard/boa-account-number` | BoA account number | +| `/payments-dashboard/boa-company-id` | CashPro company ID (check management) | +| `/payments-dashboard/boa-bank-id` | BoA routing number | +| `/payments-dashboard/slack-bot-token` | Slack Bot OAuth token | + +## Scripts + +| Script | Purpose | +|--------|---------| +| `scripts/test-boa-sandbox.js` | One-off sandbox connectivity test for both CashPro APIs | +| `scripts/seed-from-csv.js` | Seed DynamoDB from a local CSV file | +| `scripts/seed-bank-status.js` | Seed bank clear status data into DynamoDB | + +## Deployment + +```bash +sam build +sam deploy --guided +``` + +The `BOA_BASE_URL` environment variable in `template.yaml` controls whether Lambdas hit production (`https://api.bofa.com`) or sandbox (`https://api-sb.bofa.com`). All other BoA config is read from SSM at runtime. diff --git a/src/fetchBoaTransactions.js b/src/fetchBoaTransactions.js index 489f73d..08004f2 100644 --- a/src/fetchBoaTransactions.js +++ b/src/fetchBoaTransactions.js @@ -14,12 +14,36 @@ async function getSSMParam(name) { 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 [apiToken, accountNumber] = await Promise.all([ - getSSMParam(process.env.BOA_API_TOKEN_PARAM), + 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); @@ -30,12 +54,12 @@ export const handler = async () => { method: "POST", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${apiToken}`, + Authorization: `Bearer ${bearerToken}`, }, body: JSON.stringify({ - accounts: [{ accountNumber, bankId: "BOFAFRPP" }], fromDate: dateStr, toDate: dateStr, + accounts: [{ accountNumber, bankId }], }), }); @@ -45,10 +69,14 @@ export const handler = async () => { } const data = await res.json(); - const transactions = data.accountTransactions?.transactions || []; + + // 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 = transactions.filter( + const relevant = allTransactions.filter( (t) => t.transactionCode === "255" || t.transactionCode === "475" ); @@ -74,12 +102,6 @@ export const handler = async () => { 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) { diff --git a/src/processPaymentCsv.js b/src/processPaymentCsv.js index 8d1b9ba..7f5f2c6 100644 --- a/src/processPaymentCsv.js +++ b/src/processPaymentCsv.js @@ -17,6 +17,25 @@ async function getSSMParam(name) { 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; +} + // Convert MM/DD/YYYY to YYYY-MM-DD function toISODate(mdyDate) { const parts = String(mdyDate).split("/"); @@ -139,18 +158,22 @@ export const handler = async (event) => { // Submit to CashPro if there are any new issues or cancels if (newChecks.length || cancelChecks.length) { - const [apiToken, accountNumber, companyId] = await Promise.all([ - getSSMParam(process.env.BOA_API_TOKEN_PARAM), + const [appId, clientId, clientSecret, accountNumber, companyId] = await Promise.all([ + getSSMParam(process.env.BOA_CHECK_MGMT_APP_ID_PARAM), + getSSMParam(process.env.BOA_CHECK_MGMT_CLIENT_ID_PARAM), + getSSMParam(process.env.BOA_CHECK_MGMT_SECRET_PARAM), getSSMParam(process.env.BOA_ACCOUNT_NUMBER_PARAM), getSSMParam(process.env.BOA_COMPANY_ID_PARAM), ]); + const bearerToken = await getAccessToken(appId, clientId, clientSecret); + const submitToBoA = async (items, action) => { const issueList = items.map((item) => ({ accountNumber, + issueAction: action, checkNumber: item.checkNumber, amount: item.amount, - issueAction: action, issueDate: item.issueDate, })); @@ -160,7 +183,7 @@ export const handler = async (event) => { method: "POST", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${apiToken}`, + Authorization: `Bearer ${bearerToken}`, companyId, }, body: JSON.stringify({ issueList }), @@ -181,11 +204,11 @@ export const handler = async (event) => { }; if (newChecks.length) { - await submitToBoA(newChecks, "add_issue"); + await submitToBoA(newChecks, "add_Issue"); } if (cancelChecks.length) { - await submitToBoA(cancelChecks, "cancel_issue"); + await submitToBoA(cancelChecks, "cancel_Issue"); } } diff --git a/template.yaml b/template.yaml index 5c7febd..f9b9111 100644 --- a/template.yaml +++ b/template.yaml @@ -134,8 +134,10 @@ Resources: Timeout: 120 Environment: Variables: - BOA_BASE_URL: https://sandbox.cashpro.bankofamerica.com - BOA_API_TOKEN_PARAM: /payments-dashboard/boa-api-token + BOA_BASE_URL: https://api.bofa.com + BOA_CHECK_MGMT_APP_ID_PARAM: /payments-dashboard/boa-check-mgmt-app-id + BOA_CHECK_MGMT_CLIENT_ID_PARAM: /payments-dashboard/boa-check-mgmt-client-id + BOA_CHECK_MGMT_SECRET_PARAM: /payments-dashboard/boa-check-mgmt-token BOA_ACCOUNT_NUMBER_PARAM: /payments-dashboard/boa-account-number BOA_COMPANY_ID_PARAM: /payments-dashboard/boa-company-id VpcConfig: @@ -160,7 +162,11 @@ Resources: - DynamoDBCrudPolicy: TableName: !Ref DashboardTable - SSMParameterReadPolicy: - ParameterName: payments-dashboard/boa-api-token + ParameterName: payments-dashboard/boa-check-mgmt-app-id + - SSMParameterReadPolicy: + ParameterName: payments-dashboard/boa-check-mgmt-client-id + - SSMParameterReadPolicy: + ParameterName: payments-dashboard/boa-check-mgmt-token - SSMParameterReadPolicy: ParameterName: payments-dashboard/boa-account-number - SSMParameterReadPolicy: @@ -220,10 +226,12 @@ Resources: - !Ref LambdaSecurityGroup Environment: Variables: - BOA_BASE_URL: https://sandbox.cashpro.bankofamerica.com - BOA_API_TOKEN_PARAM: /payments-dashboard/boa-api-token + BOA_BASE_URL: https://api.bofa.com + BOA_REPORTING_APP_ID_PARAM: /payments-dashboard/boa-reporting-app-id + BOA_REPORTING_CLIENT_ID_PARAM: /payments-dashboard/boa-account-info-client-id + BOA_REPORTING_SECRET_PARAM: /payments-dashboard/boa-account-info-token BOA_ACCOUNT_NUMBER_PARAM: /payments-dashboard/boa-account-number - BOA_COMPANY_ID_PARAM: /payments-dashboard/boa-company-id + BOA_BANK_ID_PARAM: /payments-dashboard/boa-bank-id Events: DailySchedule: Type: Schedule @@ -235,11 +243,15 @@ Resources: - DynamoDBCrudPolicy: TableName: !Ref DashboardTable - SSMParameterReadPolicy: - ParameterName: payments-dashboard/boa-api-token + ParameterName: payments-dashboard/boa-reporting-app-id + - SSMParameterReadPolicy: + ParameterName: payments-dashboard/boa-account-info-client-id + - SSMParameterReadPolicy: + ParameterName: payments-dashboard/boa-account-info-token - SSMParameterReadPolicy: ParameterName: payments-dashboard/boa-account-number - SSMParameterReadPolicy: - ParameterName: payments-dashboard/boa-company-id + ParameterName: payments-dashboard/boa-bank-id - Version: "2012-10-17" Statement: - Effect: Allow